Chapter 8: Handling errors

Error (or exception) handling is an essential feature of writing all but trivial programs. Let's face it – s**t happens, and sometimes the best-written programs encounter errors. Well-written code handles errors gracefully and as early as possible.

Over the years, two main 'approaches' to error handling have emerged. Those advocating the LBYL approach (Look before you leap) support validating every of data well before they are used and only use data that has passed the test. LBYL code is lengthy, and looks very solid. In recent years, an approach known as EAFP has emerged, asserting the old Marine Corps motto that it is easier to ask forgiveness than permission. EAFP code relies heavily on exception handling and try/catch constructs to deal with the occasional consequences of having leapt before looking. While EAFP is generally regarded with more favour in recent years than LBYL, especially in the Python community, which all but adopted it as its official mantra, both approaches have merits (and serious drawbacks). Julia is particularly suited to an amalgam of the two methods, so whichever of them suits you, your coding style and your use case more, you will find Julia remarkably accommodating.

Creating and raising exceptions

Julia has a number of built-in exception types, each of which can be thrown when unexpected conditions occur.

Note that these are exception types, rather than particular exceptions, therefore despite their un-function-like appearance, they will need to be called, using parentheses.

Throwing exceptions

The throw function allows you to raise an exception:

    if circumference > 0
    elseif circumference == 0

As noted above, exception types need to be called to get an Exception object. Hence, throw(DomainError) would be incorrect.

In addition, some exceptions take arguments that elucidate upon the error at hand. Thus, for instance, UndefVarError takes a symbol as an argument, referring to the symbol invoked without being defined:

    julia> throw(UndefVarError(:thisvariabledoesnotexist))
    ERROR: thisvariabledoesnotexist not defined

Throwing a generic ErrorException

The error function throws a generic ErrorException. This will interrupt execution of the function or block immediately. Consider the following example, courtesy of Julia's official documentation. First, we define a function fussy_sqrt that raises an ErrorException using the function error if x < 0:

    julia> fussy_sqrt(x) = x >= 0 ? sqrt(x) : error("negative x not allowed")

Then, the following verbose wrapper is created:

    julia> function verbose_fussy_sqrt(x)
             println("before fussy_sqrt")
             r = fussy_sqrt(x)
             println("after fussy_sqrt")
             return r
    verbose_fussy_sqrt (generic function with 1 method)

Now, if fussy_sqrt encounters an argument x < 0, an error is raised and execution is aborted. In that case, the second message (after fussy_sqrt) would never come to be displayed:

    julia> verbose_fussy_sqrt(2)
    before fussy_sqrt
    after fussy_sqrt

    julia> verbose_fussy_sqrt(-1)
    before fussy_sqrt
    ERROR: negative x not allowed
     in verbose_fussy_sqrt at none:3

Creating your own exceptions

You can create your own custom exception that inherits from the superclass Exception by

    type MyException <: Exception

If you wish your exception to take arguments, which can be useful in returning a useful error message, you will need to amend the above data type to include fields for the the arguments, then create a method under Base.showerror that implements the error message:

    type MyExceptionTree <: Exception

    Base.showerror(io::IO, e::MyExceptionTree) = print(io, "Something is wrong with ", e.var, "!")

    julia> throw(MyException("this code"))
    ERROR: Something is wrong with this code.

Handling exceptions

The try/catch structure

Using the keywords try and catch, you can handle exceptions, both generally and dependent on a variable. The general structure of try/catch is as follows:

  1. try block: This is where you would normally introduce the main body of your function. Julia will attempt to execute the code within this section.
  2. catch: The catch keyword, on its own, catches all errors. It is helpful to instead use it with a variable, to which the exception will be assigned, e.g. catch err.
  3. If the exception was assigned to a variable, testing for the exception: using if/elseif/else structures, you can test for the exception and provide ways to handle it. Usually, type assertions for errors will use isa(err, ErrorType), which will return true if err is an instance of the error type ErrorType (i.e. if it has been called by ErrorType()).
  4. end all blocks.

This structure is demonstrated by the following function, creating a resilient, non-fussy sqrt() implementation that returns the complex square root of negative inputs using the catch syntax:

    function resilient_square_root(x::Number)
        catch err
            if isa(err, DomainError)

There is no need to specify a variable to hold the error instance. Similarly to not testing for the identity of the error, such a clause would result in a catch-all sequence. This is not necessarily a bad thing, but good code is responsive to the nature of errors, rather than their mere existence, and good programmers would always be interested in why their code doesn't work, not merely in the fact that it failed to execute. Therefore, good code would check for the types of exceptions and only use catch-alls sparingly.

One-line try/catch

If you are an aficionado of brevity, you should be careful when trying to put a try/catch expression. Consider the following code:

    try sqrt(x) catch y end

To Julia, this means try sqrt(x), and if an exception is raised, pass it onto the variable y, when what you probably meant is return y. For that, you would need to separate y from the catch keyword using a semicolon:

    try sqrt(x) catch; y end

finally clauses

Once the try/catch loops have finished, Julia allows you to execute code that has to be executed whether the operation has succeeded or not. finally executes whether there was an exception or not. This is important for 'teardown' tasks, gracefully closing files and dealing with other stateful elements and resources that need to be closed whether there was an exception or not.

Consider the following example from the Julia documentation, which involves opening a file, something we have not dealt with yet explicitly. open("file") opens a file in path file, and assigns it to an object, f. It then tries to operate on f. Whether those operations are successful or not, the file will need to be closed. finally allows for the execution of close(f), closing down the file, regardless of whether an exception was raised in the code in the try section:

    f = open("file")
        # operate on file f

It's good practice to ensure that teardown operations are executed regardless of whether the actual main operation has been successful, and finally is a great way to achieve this end.

Advanced error handling

info and warn

We have seen that calling error will interrupt execution. What, however, if we just want to display a warning or an informational message without interrupting execution, as is common in debugging code? Julia provides the info and warn functions, which allow for the display of notifications without raising an interrupt:

    julia> info("This code is looking pretty good.")
    INFO: This code is looking pretty good.

    julia> warn("You're not looking too good. Best check yourself.")
    WARNING: You're not looking too good. Best check yourself.

rethrow, backtrace and catch_backtrace

Julia provides three functions that allow you to delve deeper into the errors raised by an operation.

  • rethrow, as the name suggests, raises the last raised error again,
  • backtrace executes a stack trace at the current point, and
  • catch_backtrace gives you a stack trace of the last caught error.

Consider our resilient square root function from the listing above. Using rethrow(), we can see exceptions that have been handled by the function itself:

    julia> resilient_square_root(-2.345)
    0.0 + 1.5313392831113555im

    julia> rethrow()
    ERROR: DomainError
     in resilient_square_root at none:3

As it's evident from this example, rethrow() does not require the error to be actually one that is thrown - if the error itself is handled, it will still be retrieved by rethrow().

backtrace and catch_backtrace are functions that return stack traces at the time of call and at the last caught exception, respectively:

    julia> resilient_square_root(-4)
    0.0 + 2.0im

    julia> x^2 - 2x + 3

    julia> backtrace()
    13-element Array{Ptr{Void},1}:
     Ptr{Void} @0x00000001013cbfae
     Ptr{Void} @0x000000010349ec30
     Ptr{Void} @0x000000010349ebb0
     Ptr{Void} @0x00000001013776e8
     Ptr{Void} @0x00000001013c6982
     Ptr{Void} @0x00000001013c5203
     Ptr{Void} @0x00000001013d4abd
     Ptr{Void} @0x000000010137cdfd
     Ptr{Void} @0x0000000103455c41
     Ptr{Void} @0x0000000103455747
     Ptr{Void} @0x00000001013776e8
     Ptr{Void} @0x0000000103451cca
     Ptr{Void} @0x00000001013cccc8

    julia> catch_backtrace()
    14-element Array{Ptr{Void},1}:
     Ptr{Void} @0x00000001013cc506
     Ptr{Void} @0x00000001013cc5a9
     Ptr{Void} @0x00000001034a58e7
     Ptr{Void} @0x00000001034a56a7
     Ptr{Void} @0x00000001013776e8
     Ptr{Void} @0x00000001013c6982
     Ptr{Void} @0x00000001013c5203
     Ptr{Void} @0x00000001013d4abd
     Ptr{Void} @0x000000010137cdfd
     Ptr{Void} @0x0000000103455c41
     Ptr{Void} @0x0000000103455747
     Ptr{Void} @0x00000001013776e8
     Ptr{Void} @0x0000000103451cca
     Ptr{Void} @0x00000001013cccc8

The first backtrace block shows the stack trace for the time after the function x^2 - 2x + 3 has been executed. The second stacktrace, invoked by the catch_backtrace() call, shows the call stack as it was at the time of the catch in the resilient_square_root function.

results matching ""

    No results matching ""