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:
- A lot of boilerplate is necessary to propagate the exception. This is actually easy to solve with the Error monad.
- 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.)