Skip to content

Either: Exception Handling with Error Information

Back to exception handling. The last issue we need to address is how we can provide the exception handler with information about the error that occurred, just as in Java.

To do so, we need a different monad. A failure in the Maybe monad is Nothing, which carries no information about what went wrong. Where Maybe a captures the presence or absence of a value of type a, we have another standard type, Either a b, which represents the presence of a value of type a, or of a value of type b:

data Either a b = Left a | Right b

As such, it has nothing to do with exception handling. Either a b is simply a type of values that can be either a or b. However, we can use it for exception handling. Maybe has nothing to do with exception handling either. It is our interpretation of Nothing as failure in the Monad instance of Maybe that turns Maybe into a simple exception handling monad.

To use Either for exception handling, we can define some error type, such as

data Error = IOError
           | ArithmeticError
           | ProgramBug
           | UnknownError

We could of course define Error so that its data constructors take arguments to provide additional information about each error type, such as an error message, maybe the name of the file we tried to access when an IOError occurred, etc. For simplicity, we don't do that here.

We can then define a result type

type Result a = Either Error a

A function that can fail now has the type f :: a -> Result b. If f succeeds, it returns Right r, where r is the return value of f. If f fails, it returns Left e, where e is an appropriate error value of type Error.1

To make this a useful exception handling mechanism, we want to turn Either Error, or more generally, Either e for any type e we may wish to use as an error type, into a monad that implements the same abortion logic as Maybe: we abort the computation upon the first error. Here's the corresponding Monad instance:

instance Monad (Either e) where
    return  = Right
    x >>= f = case x of
        Left  _ -> x
        Right y -> f y

It may be instructive to go back to the definition of the Monad instance for Maybe and compare it to this Monad instance for Either e. If you remember that both Nothing and Left e represent failure, and Just y and Right y represent success, then you will see that these two definitions capture the exact same logic.

To make Either e a monad, we also need it to be a functor:

instance Functor (Either e) where
    fmap f (Left  e) = Left e
    fmap f (Right x) = Right (f x)

As promised, we won't verify that these definitions satisfy the functor and monad laws. Again, if you look at the Functor instance of Maybe, this is a direct translation for Either e: We view Either e r as a container of values of type r—only such a container may also store some value of type e, and no value of type r in this case. fmap f xs should apply f to every value of type r in the container. Thus, if our container is Right x, then we obtain the container Right (f x) by applying f to x. If the container is Left e, then there is no value of type r in the container to which we could apply f, so we get the original container back, Left e.

To turn Either e into a proper exception handling monad, we once again want to add a function catch that allows us to write f `catch` g to implement a computation that runs f and, if f fails, uses g as the error handler. Only now, if f fails, it provides information about the error by returning Left e, where e is an appropriate error value. We want to pass this error value e to g to allow it to implement a more fine-grained, error-dependent recovery strategy. Thus, we want catch to have the type

catch :: Either e r -> (e -> Either e r) -> Either e r

Here's the implementation:

f `catch` g = case f of
    Left e -> g e
    r      -> r

If f fails, returns Left e, we invoke our error handler g on f's error code e. Whatever the result of the error recovery is, is the result of the whole computation. If f succeeds, returns Right x, for some value x of type r, we simply return this result. This is what the second branch of our case expression does.

If e = Error, our exception handler could for example do something like:

g :: Error -> Either Error r
g e = case e of
    IOError -> -- We know how to recover from IO errors.  Let's return some
               -- default result def
               Right def
    _       -> -- We don't know how to recover from any of the other error types.
               -- We'll pass them on
               Left e

And with this, we have ourselves proper exception handling, all without the need for built-in support from the language's runtime system. It is enough to make Either e a monad.


  1. In Rust, the type equivalent to Either is called Result, and its two data constructors are called Ok and Err, corresponding to our use of Right and Left here. When using Result to represent results of computations that can fail, the naming of the type and of its data constructors in Rust makes it much clearer that we're implementing exception handling using this type. In Haskell, Either is a type that is used generally to represent the presence of a value of one of two types, not just to represent success or failure. Thus, the names of the type and of its two data constructors are more generic than in Rust.

    The module Control.Monad.Trans.Except from the transformers package provides a monad Except that behaves exactly like the Either monad we define here, but its naming makes it clearer that it is used for exception handling.