(This post seems almost too obvious to write, but I couldn’t find any other instances of people talking about this kind of pattern, or any libraries. Pointers welcome!)

If you’ve written code in Java, Python, or some other language with ubiquitous exceptions, then you are probably familiar with stack traces. Stack traces are great for a developer because they give you more contextual information about where in your code an error occurred, and often this can be enough to help you pin down the bug.

But what about in Haskell?

Adding context to MonadError

Haskell does have exceptions, and they do now have stack traces. However, most Haskellers frown on using exceptions for anything other than tricky IO problems or assertion failures, since they pollute the purity of the code. Rather, the advice is to return an Either (or, getting a bit fancier, use MonadError from mtl).

But using this approach gives you no contextual information at all! The error value which you return is created at the site of the error and then short-circuited all the way back up to the top level, with no additional information added.

Here’s a small example, with a little arithmetic expression evaluator that can throw divide-by-zero errors

{-# LANGUAGE FlexibleContexts #-}
import Control.Monad.Except

data Expr = Const Int | Plus Expr Expr | Minus Expr Expr | Div Expr Expr

instance Show Expr where
    show (Const i) = show i
    show (Plus e1 e2) = "(" ++ (show e1) ++ " + " ++ (show e2) ++ ")"
    show (Minus e1 e2) = "(" ++ (show e1) ++ " - " ++ (show e2) ++ ")"
    show (Div e1 e2) = "(" ++ (show e1) ++ " / " ++ (show e2) ++ ")"

data Error = DivByZeroError

instance Show Error where
    show DivByZeroError = "division by zero"

eval :: (MonadError Error m) => Expr -> m Int
eval e = case e of 
    Const i -> pure i
    e@(Plus e1 e2) -> (+) <$> eval e1 <*> eval e2
    e@(Minus e1 e2) -> (-) <$> eval e1 <*> eval e2
    e@(Div e1 e2) -> do
        e1' <- eval e1
        e2' <- eval e2
        when (e2' == 0) $ throwError DivByZeroError
        pure $ e1' `div` e2'

We’re going to feed this an expression with a division by zero, but with two slightly obfuscated divisions in it, so it’s not immediately obvious which one is responsible for the error. This is exactly the sort of situation where a stack trace is useful!

printEval :: Expr -> IO ()
printEval = putStrLn . either show show . runExcept . eval

main = printEval $
      ((Const 1) `Div` ((Const 1) `Minus` (Const 1)))
      `Plus`
      ((Const 2) `Div` ((Const 1) `Plus` (Const 2)))

As we expect, we get a rather unhelpful error:

division by zero

What to do? Well, we’re not devoid of tools. In particular, catchError allows us to catch an error and rethrow a new one. We can use this to roll our own contextual error enhancement:

data Error = DivByZeroError 
           | Context String Error

instance Show Error where
    show DivByZeroError = "division by zero"
    show (Context c e) = c ++ "\n" ++ (show e)
    
eval :: (MonadError Error m) => Expr -> m Int
eval e = (case e of 
    Const i -> pure i
    e@(Plus e1 e2) -> (+) <$> eval e1 <*> eval e2
    e@(Minus e1 e2) -> (-) <$> eval e1 <*> eval e2
    e@(Div e1 e2) -> do
        e1' <- eval e1
        e2' <- eval e2
        when (e2' == 0) $ throwError DivByZeroError
        pure $ e1' `div` e2')
    `catchError` (\err -> throwError $ Context ("evaluating " ++ (show e)) err)

Here we’re using catchError to catch any errors thrown by eval, wrap them in a new error (it has to be of the same type, hence why we need a new Error constructor), and then rethrow them.

Running our program again, we now get something more helpful:

evaluating ((1 / (1 - 1)) + (2 / (1 + 2)))
evaluating (1 / (1 - 1))
division by zero

So we can see which expression is the problematic one.

Now, it’s not really fair to call this a “stack trace”: it’s more of a “context trace”, and we have to put all the information in ourselves. So it’s less useful for the case where something fails in a way you hadn’t anticipated, but it’s still very useful for cases where you’re expecting errors.

Generalising a little

We can generalise this a little bit to make what’s going on slightly clearer:

data WithContext c e = Plain e | Context c (WithContext c e)

instance (Show c, Show e) => Show (WithContext c e) where
    show (Plain e) = show e
    show (Context c e) = (show c) ++ "\n" ++ (show e)
    
withContext :: (MonadError (WithContext c e) m) => c -> m a -> m a
withContext c act = catchError act $ \err -> throwError $ Context c err
    
eval :: (MonadError (WithContext String Error) m) => Expr -> m Int
eval e = withContext ("evaluating " ++ (show e)) $ case e of 
    Const i -> pure i
    e@(Plus e1 e2) -> (+) <$> eval e1 <*> eval e2
    e@(Minus e1 e2) -> (-) <$> eval e1 <*> eval e2
    e@(Div e1 e2) -> do
        e1' <- eval e1
        e2' <- eval e2
        when (e2' == 0) $ throwError $ Plain DivByZeroError
        pure $ e1' `div` e2'

This is slightly nicer at the cost of having to write WithContext String Error in your constraint, and throw Plain errors when you aren’t adding contexts.

If we actually wanted to make this a library we could improve things even more by returning a prettyprinter Doc, or maybe offering Template Haskell splices to throw errors tagged with source locations.