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.
<TODO: Table of exceptions and their meanings>
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:
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:
Throwing a generic ErrorException
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
:
Then, the following verbose wrapper is created:
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:
Creating your own exceptions
You can create your own custom exception that inherits from the superclass Exception
by
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:
Handling exceptions
The try
/catch
structure
try
/catch
structureUsing 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:
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
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:
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:
finally
clauses
finally
clausesOnce 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:
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
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
macros, which allow for the display of notifications without raising an interrupt:
See Logging in the Julia documentation for more information.
rethrow
, backtrace
and catch_backtrace
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:
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:
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.
Last updated
Was this helpful?