Adventures in Haskell – Error handling

I fell in love with Haskell almost the day I started using it.  It’s not perfect, though, and one of its weaker areas is error/exception handling.  I’m not going to cover all ways to return errors.  For more details, see 8 ways to report errors in Haskell.

Haskell has an Exception class in the Control.Exception module, but this can only be caught within the IO monad, which is an onerous restriction.  In my (un)experience, a more flexible approach is to have your function return Either String a, where a is whatever type it would return normally.  In this case, the function would evaluate to Left "error text" on an error, or Right 2 (if it was going to return 2, for example).   There are two issues:

  1. A lot of boilerplate is necessary to propagate the exception.  This is actually easy to solve with the Error monad.
  2. Haskell has no support whatsoever for stack traces.  These can be hacked in, but it requires manual effort.

To propagate the exceptions, use the Error monad.  You can use pretty much any error type with the Error monad as long as you set up up first.  Either String a is set up already, so it’s easy to use.   Let’s say you have functions f1 and f2 which accept a non-negative integer and return Either String Int.  To sum them, define mysum as follows:

-- This uses the Error monad even though we don't say so anywhere.  Type inference is awesome!
mysum :: Int -> Either String Int
mysum x =
  do
    when (x < 0) (throwError "x is negative")
    x1 <- f1 x
    x2 <- f2 x
    return (x1 + x2)

This is essentially equivalent to:

-- Ugly boilerplate, even for only two function invocations.
mysum :: Int -> Either String Int
mysum x =
  if x < 0
  then Left "x is negative"
  else case f1 x of
         Left e -> Left e
         Right x1 -> case f2 x of
           Left e -> Left e
           Right x2 -> Right (x1 + x2)

As I said, the problem of stack traces is more difficult.  I’m not aware of a standard or convention, but one way to handle it is to manually add prefixes to a list of some sort, like String (which is a list of Char), which means we can still use Either String a.  A helper function I wrote makes this a little easier:

nestError :: (MonadError [p] m) => [p] -> m a -> m a
nestError prefix a = catchError a (\e -> throwError (prefix ++ e))

This would be used like so:

mysum :: Int -> Either String Int
mysum x =
  nestError "Error in mysum: " (do
    when (x < 0) (throwError "x is negative")
    x1 <- f1 x
    x2 <- f2 x
    return (x1 + x2)
  )

If the second argument returns normally, nestError just returns the value.  If there’s an error, it adds the prefix.  For example, if x was negative, the final result would be Left "Error in mysum: x is negative".  If f1 had the error “foo”, the final result would be Left "Error in mysum: foo".

Clearly, this pales compared to Java’s exceptions, which are the best of any language I’ve used.  However, tricks like this make Haskell’s error handling a bit easier to use.

(Disclaimer: I am not a Haskell expert.  There may be (and probably are) better ways to do this.)

2 Comments

  1. Posted April 2, 2013 at 1:52 am | Permalink

    Things become considerably easier if you strictly distinguish between programming errors and exceptions:
    http://www.haskell.org/haskellwiki/Error_vs._Exception

    You use an exception handling technique (Either) for treatment of a programming error (calling mySum with a negative number, which is not allowed). You would better use a type that only allows non-negative numbers. You may use Word, but that overflows silently. You may also use the non-negative package.

  2. Posted April 2, 2013 at 10:32 am | Permalink

    You’re right that I’m conflating programming errors with runtime exceptions, but making that distinction doesn’t make things easier. The example I gave was intended to be easy.

    Consider writing a version of printf that should fail for invalid format strings. To do what you suggest, you would have to create a type for valid format strings, make the constructor private, and write a function that takes a String and returns a Maybe ValidFormatString. This is a lot of work, but may be acceptable in this case. There are more complex examples, though, where you would have to create a “valid input” type that is only useful for a single function. The end result is essentially the same as having the function return Maybe Result, where it returns Nothing for invalid input.

    The bottom line, though, is that errors will happen (programmers aren’t perfect), and Haskell doesn’t provide a simple, general facility for finding the execution path that led to the error.

Post a Comment

Your email is never shared. Required fields are marked *

*
*