Managing Exceptions with ResultTypes
Managing Exceptions with ResultTypes¶
In this post, we want to briefly review some examples of exception handling, present the ResultTypes package as an alternative and finally, show how these mechanisms can cooperate nicely.
Exceptions in Julia¶
Julia supports Exceptions with mechanisms well-known from other languages:
A function can throw
an exception when facing invalid input or a situation that is in some otherwise unexpected.
The caller can then handle the exception by wrapping the function call in a try
/catch
block.
If not handled, the exception will travel upwards on the call stack all the way to the REPL (or even stop the process) and print a stack trace. Julia offers several built-in exception types, one can also define custom Exception
types, or simply call the error
function as a shortcut to throw an ErrorException
with a given message.
HTTP example: GET¶
Let's use a simple example function that wraps HTTP.get
and checks the URL for the transport protocol to demonstrate usage of exceptions.
using HTTP
"GET given URL, if using HTTPS, else throw exception."
function https_get1(url)
if !startswith(url, "https://")
throw(DomainError("$url: only HTTPS requests allowed!"))
end
return HTTP.get(url)
end
# Test it with a happy case, printing the result.
https_get1("https://httpbin.org/status/200")
# Test it with an error case, showing the stack trace and exception.
https_get1("http://httpbin.org/status/200")
In our case, we have an idea of how to fix the error causing issue, by changing the URL.
Let's reiterate on our https_get
function by handling the exception:
"GET with URL, enforce HTTPS via exception handling."
function https_get2(url)
try
return https_get1(url)
catch e
if isa(e, DomainError)
return https_get1(replace(url, "http" => "https", count=1))
else
rethrow(e)
end
end
end
# Try previous error case again, which is now fixed:
https_get2("http://httpbin.org/status/200")
# Try another error case, where an exception is thrown by the `HTTP` package,
# which we don't handle. Here, we are again shown the complete (because of rethrow)
# and long stack trace, full of HTTP.jl internals.
https_get2("https://httpbin.org/status/404")
ResultTypes¶
There is an alternative pattern of error handling that does not use exceptions, but communicates about success or failures with structural return values from functions. Rather than simply returning the actual result of the function, that value is wrapped in a parameterized type that could also contain an error description, for example in the form of an exception value.
The package ResultTypes provides a Julia implementation of this pattern and the README shows a usage example and benchmarks with performance benefits.
Another possible advantage is that it nudges developers to be more explicit about errors that could happen with function calls and might lead to dealing with the errors in a more local context. Further, the results (including errors) are just values (nothing special/magical) and can be dealt with programmatically. These points are also discussed in a blog post about error handling in Golang.
GET example with ResultTypes¶
Let's try to see what our example GET
wrapper would look like if we used ResultTypes
rather than throwing an exception.
using ResultTypes
"GET given URL, if using HTTPS, else return error value."
function https_get3(url)::Result{HTTP.Messages.Response, DomainError}
if !startswith(url, "https://")
return DomainError(url, "Insecure protocol!")
end
return HTTP.get(url)
end
# Try happy case again, this time with wrapped result:
https_get3("https://httpbin.org/status/200")
# Try bad case again, resulting in error value.
https_get3("http://httpbin.org/status/200")
# Try third-party exception, which is still thrown :-\
https_get3("https://httpbin.org/status/404")
As we have seen in the last example, even if we decide to use ResultTypes
in our code, we are not safe from exceptions being thrown in the calls below.
Does that limit the scope of the pattern and its implementation with ResultTypes
? Can we only use it internally within our libraries, but still deal with exceptions bubbling up from other code?
What should we do about return values in user-facing functions in our library API? We can not expect everybody to learn about and deal with two different patterns of error handling just to use our library.
Systematic Error-Handling in C++¶
I was reminded of ResultTypes
recently, when I researched error handling patterns in C++, and stumbled upon a talk titled "Systematic Error-Handling in C++" by Andrei Alexandrescu (video, slides).
In the first half of the presentation, he motivates and sketches the implementation of an Expected<T>
type which is equivalent to what is done in ResultTypes
. In addition to the advantages shown above, he also mentions dealing with errors across threads (multiple simultaneous exceptions being thrown), but I'm not sure how this applies to Julia.
Most interesting to me was slide 27 (Icing) with this definition of fromCode
:
template <class F>
static Expected fromCode(F fun) {
try {
returnExpected(fun());
} catch(...) {
return fromException();
}
}
auto r = Expected<string>::fromCode([] {...});
This provides a bridge between the worlds of exception-throwing and result-returning. Thrown exceptions are captured and instead returned as error values. This applies in particular to the case where we call functions from third-party libraries.
Wrapping Callees¶
We repeat our example function, but attempt to capture all exceptions (including those from HTTP
) and work with result values exclusively.
"GET given URL using HTTPS or return error value."
function https_get4(url)
if !startswith(url, "https://")
return ErrorResult(DomainError(url, "Insecure protocol!"))
end
try
# Happy path: wrap result value.
return Result(HTTP.get(url))
catch e
# Turn all exceptions into error values.
return ErrorResult(e)
end
end
# Test our own error case.
https_get4("http://httpbin.org/status/200")
# Test error case in called function, also resulting in error value,
# no longer an exception with stack trace etc.
https_get4("https://httpbin.org/status/400")
Wrapping for Callers¶
Let us assume that https_get4
was our library-internal utility function, but we would like to expose a version to our library users. They are expecting exceptions, so we throw them in case of error values:
"GET from URL with HTTPS or throw exception."
function https_get5(url)
result = https_get4(url)
if ResultTypes.iserror(result)
# Error case, get exception and throw it.
throw(unwrap_error(result))
end
# Happy case, return the raw result.
return unwrap(result)
end
# Test working case, returning raw result.
https_get5("https://httpbin.org/status/200")
# Test with our error (using HTTP), now thrown as exception.
https_get5("http://httpbin.org/status/200")
# Test with third-party error, also thrown as exception.
https_get5("https://httpbin.org/status/500")
Notice that the stack trace in the last example is shallow. That is, it starts from the throw
statement in our own functions and does no longer contain the levels below from where the exception originates.
This can be seen as positive or negative, but in any case, I don't know how it could be changed. Use of rethrow
is not allowed here, because there is no try
/catch
block.
Conclusion¶
We have seen how we can bridge between the patterns of throwing exceptions and return error values, easily and in both directions. Maybe this will convince some developers to use ResultValues
in their own packages?
So far, I have only detected its use in Dispatcher.jl. Please share your experiences with error handling in Julia!