Skip to content

Maybe: Simple Exception Handling

Java, C++, Python, and a number of other languages support exception handling. A function may throw an exception to indicate that it cannot complete its computation normally. If some function f calls another function g and g throws an exception, then f immediately stops what it is doing and passes the exception on to its caller. This continues until the entire program terminates and we get the usual scary print out of the exception and a stack trace that we are used to when a Java program crashes.

Languages that support exception handling also have a mechanism to catch exceptions to prevent the program from aborting and deal with exceptions gracefully, recover from them. In Java, we would do this using

try {
    // Some code that may fail
} catch (Exception e) {
    // Recover from the exception
}

The code inside the try block may fail. If it doesn't, then the catch block is never executed. If some statement inside the try block throws an exception, control is immediately transferred to the catch block. This block may either decide that it can't deal with the exception and rethrow it, or it can deal with the exception gracefully, so execution of the current function continues normally with whatever comes after the try-catch block.

Haskell also has proper exceptions and exception handling, but Haskell programmers tend to use them rarely. That's exactly because we have monads such as Maybe and its more powerful cousin Either Error to implement exception handling purely functionally. There is no need to reach deep into the innards of Haskell's runtime system, which is what proper exception handling inevitably does. Interestingly, Rust, a modern language focused on safety and performance, does not include any exception handling mechanism whatsoever. Instead, it uses Option<T> or Result<T> to return results of type T from functions that may fail. A successful computation returns Some(x) or Ok(x), where x is a value of type T. An unsuccesful computation returns None or Err(e), where e is an appropriate error value. Rust requires us to either handle errors when they occur, by dealing with a None or Err(e) return value on the spot or by aborting the function and returning the error value. Rust has a special operator ? built into the language that allows us to unwrap an Option<T> or Result<T> to gain access to the value they store. If the value is Some(x) or Ok(x), then the result of this operator is simply x. If it is None or Err(e), then the function aborts and returns this error value. Option is Rust's equivalent of the Maybe type, Result is the equivalent of Haskell's Either type, and the abortion logic of ? is exactly the same that will be implemented by our Monad instances for Maybe and Either Error. The difference is that ? is built into the Rust language itself, while the Monad instances for Maybe and Either Error are part of the standard library, that is, they are implemented in plain Haskell. We can do this because monads allow us to define what it means to execute a sequence of steps. Given that Rust is an imperative language with the logic of sequencing steps hardwired into the language, building the ? operator directly into the language is the only choice.

So how can we capture exception handling using a monad? We'll take it in stages:

  • First, we want a way to express that steps in a computation can fail and that the whole computation fails as soon as one of its steps fails. This is exactly the idea of propagating exceptions up the call stack and aborting the whole program if we never catch the exception.

  • Next, we show how to implement something like Java's catch. In this section, we will represent success or failure using the Maybe functor. Nothing represents failure, so our implementation of catch will receive no information about what exactly went wrong when a computation failed. In Java, you will sometimes also see exception handlers that completely ignore the available information about the type of exception that arose. Our implementation of catch in this section will be able to implement such "catch all" exception handlers.

  • In an upcoming section, we'll switch to using Either Error as a monad, where Error is our exception type that tells us what went wrong. This will then allow us to implement proper exception handling where the exception handler responds to the type of error that arose.