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:
if circumference > 0
circumference/2Ï€
elseif circumference == 0
throw(DivideError())
else
throw(DomainError())
endAs 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
ErrorExceptionThe 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:
tryblock: This is where you would normally introduce the main body of your function. Julia will attempt to execute the code within this section.catch: Thecatchkeyword, 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/elsestructures, 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 iferris an instance of the error typeErrorType(i.e. if it has been called byErrorType()).endall 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/catchIf 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 warnWe 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_backtraceJulia 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,backtraceexecutes a stack trace at the current point, andcatch_backtracegives 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 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:
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?