Sublime Blog

Error Handling

May 13, 2020

At this point there are two main systems for handling errors in programming languages. Throwing exceptions or returning values. I strongly prefer returning values - more pure functional being just one reason why, however I accept that exceptions do have their place, which makes this harder to discuss as there are various factors to consider.

C programming often includes mutating input parameters to functions instead of returning a value (side effecting changes), using the return value as an error code or even setting a global error value (e.g. errno - it can be threadlocal but the data has to be accessed through a non-local variable). Whilst there are plenty of imperative languages with impure functions/methods that mutate input parameters, global error values or error code returns are considered bad style in high level languages. Even if thread safe you may not even be aware that the API sets a global error and having the return value be an error code has poor aesthetics if we want to do things like chain function calls.

The return value can be a composite of the success path and the failure path. In Go lang programming this means a tuple (value, error) where if error is not nil then the error case occurred, otherwise the success value is the success case (presumably nil if not successful).

response, err := doApiCall("...")
if err != nil {
    // handle the error, commonly just:
    return err
}
// do something with response

This style does make the error handling upfront/explicit as the caller has to receive the error value. However, the type system and compiler won’t force you to correctly check whether you got an error value or not.

Typed Return Values

The return value can be better typed, e.g. a Result (or Either in some other languages) type encapsulates a success value or error value with both types tracked by the type system. A result library even exists in python and makes full use of type annotations meaning it works well with the mypy python typechecking tool. Results are commonly used in typed functional languages (F#, Ocaml, Haskell, Elm etc.) and Rust.

from enum import Enum, auto
from typing import NewType

UserId = NewType("UserId", int)

class ApiCallError(Enum):
    NotFound = auto()
    ServerError = auto()

def do_api_call() -> Result[UserId, ApiCallError]:
    ...
    return Result.Ok(UserId(42))

The result can be queried with is_ok or is_err and then the value pulled out. In a strongly typed language it would be a compile error to try and pull the success case out when the error case occurred. The type signature makes it clear that this function returns one of UserId or ApiCallError and it can be a pure function with the usual benefits.

Results, the uglier parts

When calling third party impure code that does throw exceptions we can catch unexpected exceptions, perhaps log the error in application code, and return a typed result (or even an Optional if not providing error information). That is fine. When we do want to provide detailed error information we may end up wrapping the exception that we just caught.

from adt import adt, Case

@adt
class ApiCallErrorADT:
    NOT_FOUND: Case[str]
    SERVER_ERROR: Case[Exception]

def do_api_call() -> Result[UserId, ApiCallErrorADT]:
    try:
        ...
        return Result.Ok(UserId(...))
    except Exception as e:
        ...
        return Result.Err(ApiCallErrorADT.SERVER_ERROR(e))

Here, the adt library is used so that we can associate data with a union case - python doesn’t natively support discriminated/disjoint unions - we can’t have variable data when just using an Enum. Any wrapping/unwrapping is a bit tedious though. Better typed, but may seem like extra work when you already had an Exception instance, a type designed to contain errors. Exceptions can also have subclasses, e.g. we could have a ServerError exception, why is that not a good enough return type - Result[UserId, ServerError]? Actually, it is fine when there is only one type of error to deal with. We are returning success or an exception instead of returning success or throwing an exception.

It’s fine if this looks a bit weird, python hasn’t long had tools for using Results and Types rigorously. Naturally, it’s not as robust as a full statically compiled language. In this example, why not just use an exception hierarchy and return (not throw) e.g. an ApiException that has subclasses like NotFound or GenericServerError? That’s more a discussion around designing with types - do you want an open extensible list of error cases or a closed documented explicit list of all the error cases in one place? I’d default to a closed concrete definition, particularly useful for differentiating domain errors to branch on in a strongly typed way. It’s also worth mentioning that python does not have pattern matching, the mypy type checker does not currently ensure that you handle every case of an enum. The adt decorator does generate a match method on the class it decorates:

api_call_error.match(
    not_found=lambda not_found_message: ...,
    server_error=lambda server_error_exception: ...
)

Works, though a bit different to first class match expressions in languages actually supporting pattern matching. Syntactic verbosity was present in querying the result is_ok or is_err and then pulling out the value. Languages with strong adt (algebraic data type - specifically disjoint unions) support have ways to make (monadic) error handling much terser, e.g. the ? operator in Rust.

Exceptions

Exceptions are the default in mainstream Object Oriented languages, including in Python. Two big problems with exceptions are the lack of proper typing and the complete violation of functional purity.

Functional first design assumes we are striving for functional purity. Even an almost total pure function that has a deterministic output for every input except one that throws an exception is impure. It’s not referentially transparent but rather dependent upon what code if any handles the exception further up the call stack. The unwinding of the stack frame will wreck any other parent functions that try to be pure or just complete their operations.

The lack of proper typing, assuming we actually appreciate static typing as a tool for modelling and constraining the inputs and outputs to functions, is a barrier to understanding. No longer does the function just return what it says it does in the return type signature but some number of exceptions could be thrown which may only be apparent by reading the documentation or the source code. Often it will be insufficient to look at just the source code of that one function but all the functions that are called from that point must be understood. Checked exceptions in Java are the only typed-in-signature exceptions in a mainstream OO language (ignoring the the little used throw annotations in C++ libraries). It can be interesting to know why they are really not popular in Java though - see The Problem With Checked Exceptions.

Exceptions, the good parts

Exceptions do encapsulate a lot of often useful error information in the form of a stack trace (trace back). The trace back is conveniently created and shown without effort on the programmers part when unhandled or explicitly printed. Domain error creation utility libraries in Rust language programming can provide this, but it’s not as immediate (note Rust has no exceptions).

For prototyping it is convenient to just fail fast instead of caring about describing all possible error paths with domain error types. Failing fast makes sense at other times, such as truly exceptional code paths, e.g. invalid configuration on application startup or the program getting into a state that it is unable to recover from.

For libraries, I argue that it’s more important to use return values - libraries should be clear about their errors in the type signatures so different applications can handle the errors as is suitable for them, which could then include throwing an exception. The big question though is whether in a language throwing exceptions it’s worth it to wrap that exception in a domain error. A lot of the time that exception will have nothing to do with your domain and there is nothing that can be done with it except pass it on in a wrapping type.

Overall

I’d throw an exception or let exceptions be propagated if the event is actually “exceptional” enough that we can’t do anything useful with it (or if it is an error pertinent to our application domain then we have actively choosen to ignore it for now). Things we can’t do much about generally cover the operating environment. What is or is not part of our domain of concerns may need adjusting if exceptions come up that we can reasonably squash into a domain error. Actually doing something useful with uncaught exceptions should be fairly trivial to setup once. In a web service the framework should be catching and logging the exception, and returning an appropriate 500 server error. In an actor based system with supervisor hierarchies like Erlang OTP or Akka exceptional events would cause the crashed processes to be restarted anew.

When letting exceptions happen deliberately it maybe useful to document what raised exceptions are being allowed as opposed to just not known about. A final case where using a exception for brevity could be appropriate is when the exception is defined, thrown and caught within the scope of one module’s private functions and documented to do so - local control flow, the more local the better. It will however, not be documented by the type system and be messier to test. Exceptions shouldn’t flow across two different public functions.

When using results and typed errors separating out and modelling all your domain errors can make domain decison logic clearer (especially if the domain errors are a concrete closed disjoint union), but exceptions coming from third party libraries or generic operating environment errors will be handled by (1) wrapping it, (2) letting it throw or (3) translating it to a domain error. On balance (1) may have little value so we are left with letting it throw which includes all the details from the error source or finding a sensible domain error which will probably squash some of the error detail. When (3) is reasonable, go for it, and when it’s not, let it throw and try not to depend on information in the exception as the exception hierarchy is open - it would be hard to reason about what can and should be handled from what exceptions where.


Notes from a software engineer with two decades working in various industries - games, poker and gambling, music streaming and telecommunications. Likes fast code and functional programming. Based in the UK.

github mark
© 2024