Exception handling

Unexpected behaviour may often occur during running code, which may lead to the situation that some function cannot return a reasonable value. Such behaviour should be handled by either terminating the program with a proper diagnostic error message or allowing that code to take appropriate action.

In the following example, we define a factorial function in the same way as we did in the Short-circuit evaluation section.

function fact(n)
    isinteger(n) && n >= 0 || error("argument must be non-negative integer")
    return n == 0 ? 1 : n * fact(n - 1)
end

We use the error function, which throws the ErrorException if the input argument does not meet the given conditions. This function works quite well and returns a reasonable error message for incorrect inputs.

julia> fact(1.4)
ERROR: argument must be non-negative integer

julia> fact(-5)
ERROR: argument must be non-negative integer

However, it is better to use error messages as descriptive as possible. In the case above, the error message can also include the argument value. Julia provides several predefined types of exceptions that can be used to create more descriptive error messages. In our example, we want to check whether the argument is a non-negative integer. The more specific DomainError can do this.

function fact(n)
    isinteger(n) && n >= 0 || throw(DomainError(n, "argument must be non-negative integer"))
    return n == 0 ? 1 : n * fact(n - 1)
end

We must use the throw function because the DomainError(x, msg) function only creates an instance of the type DomainError, but it does not raise an error.

julia> fact(1.4)
ERROR: DomainError with 1.4:
argument must be non-negative integer

julia> fact(-5)
ERROR: DomainError with -5:
argument must be non-negative integer

The error message now contains a short description, the input value, and the type of exception. Now imagine that due to an error, the fact function is used to calculate the factorial from a string.

julia> fact("a")
ERROR: MethodError: no method matching isinteger(::String)
Closest candidates are:
  isinteger(::BigFloat) at mpfr.jl:859
  isinteger(::Missing) at missing.jl:100
  isinteger(::Integer) at number.jl:20
  ...
Stacktrace:
 [1] fact(::String) at ./REPL[1]:2
 [2] top-level scope at REPL[2]:1

In this case, the MethodError is raised for the isinteger function. Since the DomainError function is not even called, the error says nothing about the fact function. We can track that the error occurs when calling the fact function using the Stacktrace section located under the error message. The Stacktrace provides us with an ordered list of function calls (starting from the last one) that preceded the error. In this case, the last function call before the error is fact(::String). It tells us that the error occurs in the function fact with a string as the input argument. In this particular case, it makes sense to define factorial function only for real numbers. This can be done by entering the input type in the function declaration.

function fact_new(n::Real)
    isinteger(n) && n >= 0 || throw(DomainError(n, "argument must be non-negative integer"))
    return n == 0 ? 1 : n * fact(n - 1)
end

This function declaration will only work for subtypes of Real. Otherwise, the MethodError will occur.

julia> fact_new("aaa")
ERROR: MethodError: no method matching fact_new(::String)
[...]

The MethodError provides two important pieces of information. First, it states that the fact_new function is not defined for arguments of type String. Second, it shows the list of methods closest to the one we called. In this case, the fact_new function has only one method, which works for any subtype of Real. This can be verified by using the methods function.

julia> methods(fact_new)
# 1 method for generic function "fact_new" from Main:
 [1] fact_new(n::Real)
     @ none:1

A more precise description and a list of all predefined exception types can be found in the official documentation.