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
circumference/2π
elseif circumference == 0
throw(DivideError())
else
throw(DomainError())
end
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
end
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
1.4142135623730951
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
end
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
var::String
end
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:
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.catch
: Thecatch
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
.- 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 useisa(err, ErrorType)
, which will return true iferr
is an instance of the error typeErrorType
(i.e. if it has been called byErrorType()
). 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)
try
sqrt(x)
catch err
if isa(err, DomainError)
sqrt(complex(x))
end
end
end
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")
try
# operate on file f
finally
close(f)
end
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, andcatch_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 throw
n - 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
11
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.