Random code snippets, projects and musings about software from Eric Kidd, a developer and entrepreneur. You're welcome to contact me!

8 ways to report errors in Haskell

Mar 10, 2007 • by Eric Kidd

Haskell is a marvellous language, but there are some things I don't like about it. My least favorite: Haskell has no fewer than 8
different APIs for reporting errors.

To make a bad situation worse, the choice of API varies between
popular libraries. To give a particularly unfortunate example,
Network.URI.parseURI and Network.HTTP.simpleHTTP report
errors in entirely different ways, turning a "download this URL" program into a page of code,
nearly half of which is devoted to dealing with various kinds of errors. (The rest is boilerplate that could be refactored into a nice wrapper.)

Let's begin with a toy function, the simplest possible program that could
actually fail:

myDivxy=x/y

As every algebra student knows, we can't divide by zero. Using this
function as our example, let's take a look at all the different ways we
can implement error-reporting in Haskell.

1. Use error

The most popular way to report errors in Haskell is error,
which works as follows:

myDiv1::Float->Float->FloatmyDiv1x0=error"Division by zero"myDiv1xy=x/y

(This is similar to the error-reporting that's built into integer division, actually.)
We can catch the error using Control.Exception.catch:

This style of error-reporting is used widely in the standard libraries,
because it's so flexible. You can find several examples in
Data.Map.

If you're writing new Haskell libraries for public consumption, and all
your errors are strings, please consider using this error-reporting
method.

5. Use MonadError and a custom error type

What if we want to keep track of specific types of errors? In that case,
we could use the error Error type class:

importControl.Monad.ErrordataCustomError=DivByZero|OutOfCheese|MiscErrorStringinstanceShowCustomErrorwhereshowDivByZero="Division by zero"showOutOfCheese="Out of cheese"show(MiscErrorstr)=strinstanceErrorCustomErrorwherenoMsg=MiscError"Unknown error"strMsgstr=MiscErrorstr

This works like the fail example, but instead of using error
messages, we use error values:

Note that this approach will work in almost any monad except the IO
monad. This approach will also fail if we start mixing libraries,
because each library will define its own set of errors, and we'll need to
write code which converts them all to our preferred error type.

This approach is used by many popular libraries, including parsec. An
unusual variant of this approach is used by Network.HTTP, which
returns values of type IO (Either ConnError a), but doesn't
make ConnError an instance of Error.

6. Use throwDyn in the IO monad

We can also use our custom error type in the IO monad, thanks
to throwDyn and catchDyn from
Control.Exception.

This relies on the fact that Exception is extensible, thanks
to its DynException constructor. If you're working in the IO
monad, this approach is almost ideal for production code: You get support
for custom exception types, it's easy to make libraries compatible, and
it's compatible will all the other IO-based examples we've
seen.

Note that this very flexible approach could generalized to
non-IO monads by making Exception an instance of
Error, and writing appropriate versions of
throwDyn and catchDyn for
MonadError. This would actually be very convenient for people
who have to work with many libraries at once. But I'll refrain from
actually providing code, because there's too many error-reporting
conventions already!

7. Use ioError and catch

This is a close cousin to the throwDyn example above. It also
relies on Exception.

myDiv7::Float->Float->IOFloatmyDiv7x0=ioError(userError"Division by zero")myDiv7xy=return(x/y)example7::Float->Float->IO()example7xy=catch(doq<-myDiv7xyputStrLn(showq))(\err->putStrLn(showerr))

This one is pretty rare, as far as I can tell.

8. Go nuts with monad transformers

Several of the error-reporting approaches we've seen are based on
non-IO monads. Most of these can can be generalized to the
equivalent monad transformers. For example, Either CustomError
a becomes:

very useful post. i have then selected version 4 and it works.
thanks for shedding light on this issue, which is confusing due to the incompatible but same name functions.

andrew

andrew u frank
wrote on Jun 04, 2009:

moving from ghc 6.8.2 to 6.10 I cannot recompile the examples given here (especially 1 and 4). What has changed in base 4.0.0.0? The error I get is

Ambiguous type variable `e’ in the constraint:
`Exception e’
arising from a use of `E.catch’

anybody can help?

andrew u frank
wrote on Jun 06, 2009:

Using ghc 6.10 (with base 4.0.0.0) the module Control.Exception becomes easier to use and seem to be quite flexible.

The only limitation I can see is that the exception raised in pure code (this is possible) cannot be caught in pure code; catching exceptions is only possible in the IO monade. I wonder, if this is a serious restriction and how difficult it would be to ovecome.
The same example with division by zero becomes (if properly formated):