Functional Programming

Functional Programming

Functional Programming

Nov 7, 2016

Exceptions Best Practices in Haskell

Exceptions Best Practices in Haskell

Exceptions Best Practices in Haskell

The content below is still correct, but has been absorbed into the more comprehensive safe exception handling tutorial document instead. I recommend reading that, which provides more information and more up-to-date library references.

Over the years, I've written a number of different documents,

tutorials, comments, and libraries on how to do proper exception

handling in Haskell. Most of this has culminated in the creation of

safe-exceptions library, which I strongly recommend everyone use. That library

contains a full tutorial which explains many of the more subtle

points of exceptions, and describes how exceptions are handled by

that library.


Overall, I consider that library uncontroversial, simply

addressing the reality of exceptions in GHC today. This blog post

is the opinionated part: how I recommend you use exceptions in

Haskell, and how to structure your code around them. There are

dissenting opinions, which is why this is an opinion blog post

instead of library documentation. However, in my experience, these

approaches are the best way to make your code robust.


This blog post is also part of the FP Complete Haskell Syllabus and part of our Haskell training.

The IO contract

A commonly stated position in the Haskell community around

exceptions goes something like "all exceptions should be explicit

at the type level, and async exceptions are terrible." We can argue

as much as we want about this point in a theoretical sense.

However, practically, it is irrelevant, because GHC has already

chosen a stance on this: it supports async exceptions, and all code

that runs in IO can have exceptions of any type which is an instance of Exception.


I'd go a step further, and say not only are we stuck with GHC's

decisions, but GHC's decisions are a great point in the design

space. I'll explain that below.


So take as a given: any code in IO can throw a

runtime exception, and any thread can be killed at any time by an

asynchronous exception.


Let's identify a few anti-patterns in Haskell exception handling, and then move on to recommended practices.

The bad

ExceptT IO anti-pattern

A common (bad) design pattern I see is something like the following:

There are (at least) three problems with this:

  1. It's non-composable. If someone else has a separate exception type HisException, these two functions do not easily compose.

  2. It gives an implication which is almost certainly false,

    namely: the only exception that can be thrown from this function is

    MyException. Almost any IO code in there

    will have the ability to throw some other type of exception, and

    additionally, almost any async exception can be thrown even if no

    synchronous exception is possible.

  3. You haven't limited the possibility of exceptions, you've only

    added one extra avenue by which an exception can be thrown.

    myFunction can now either throwE or liftIO . throwIO.

It is almost always wrong to wrap an ExceptT, EitherT, or ErrorT around an IO-based transformer stack.

Separate issue: it's also almost always a bad idea to have such

a concrete transformer stack used in a public-facing API. It's

usually better to express a function in terms of typeclass

requirements, using mtl typeclasses as necessary.


A similar pattern is

This is usually done with the idea that in the future the error type will be changed from Text to something like MyException. However, Text may end up

sticking around forever because it helps avoid the composition

problems of a real data type. However that leads to expressing

useful error data types as unstructured Text.


Generally the solution to the ExceptT IO anti-pattern is to return an Either from more

functions and throw an exception for uncommon errors. Note that

returning Either from ExceptT IO means

there are now 3 distinct sources of errors in just one

function.


Please note that using ExceptT, etc with a non-IO base monad (for example with pure code) is a perfectly fine pattern.

Mask-them-all anti-pattern

This anti-pattern goes like this: remembering to deal with async exceptions everywhere is hard, so I'll just mask them all.

Every time you do this, 17 kittens are mauled to death by the loch ness monster.

Async exceptions may be annoying, but they are vital to keeping a system functioning correctly. The timeout function

uses them to great benefit. The Warp webserver bases all of its

slowloris protection on async exceptions. The cancel function from

the async package will hang indefinitely if async exceptions are

masked. Et cetera et cetera.


Are async exceptions difficult to work with? Sometimes, yes. Deal with it anyway. Best practices include:

  • Use the bracket pattern wherever possible.

  • Use the safe-exceptions package.

  • If you have truly complex flow of control and non-linear scoping of resources, use the resourcet package.

The good

MonadThrow

Consider the following function:

If this function returns Nothing, we have no idea why. It could be because:

  1. "foo" wasn't in the map.

  2. "bar" wasn't in the map.

  3. "baz" wasn't in the map.

  4. f returned Nothing.

The problem is that we've thrown away a lot of information by having our functions return Maybe. Instead, wouldn't it be nice if the types of our functions were:

The problem is that these types don't unify. Also, it's commonly the case that we really don't need to know about why a

lookup failed, we just need to deal with it. For those cases,

Maybe is better.


The solution to this is the MonadThrow typeclass

from the exceptions package. With that, we would write the type

signatures as:


Versus the Either signature, we lose some

information, namely the type of exception that could be thrown.

However, we gain composability and unification with

Maybe (as well as many other useful instances of MonadThrow, like IO).


The MonadThrow typeclass is a tradeoff, but it's a

well thought out tradeoff, and usually the right one. It's also in

line with Haskell's runtime exception system, which does not

capture the types of exceptions that can be thrown.


Transformers

The following type signature is overly restrictive:

This can always be generalized with a usage of liftIO to:

This allows our function to easily work with any transformer on top of IO. However, given how easy it is to apply liftIO, it's not too horrible a restriction. However, consider this function:

If you want your inner function to live in a transformer on top of IO, you'll find it difficult to make it work. It can be done with lifted-base, but it's non-trivial.

Instead, it's much better to express this function in terms of

functions from either the safe-exceptions library, and get the

following more generalized type signatures:


This doesn't just apply to exception handling, but also to

dealing with things like forking threads. Another thing to consider

in these cases is to use the Acquire type from resourcet.


Custom exception types

The following is bad practice:

The problem is the usage of arbitrary string-based error

messages. This makes it difficult to handle this exceptional case

directly in a higher level in the call stack. Instead, despite the

boilerplate overhead involved, it's best to define a custom

exception type:


Now it's trivial to catch the SomethingBad exception type at a higher level. Additionally, throwM gives better exception ordering guarantees than error,

which creates an exception in a pure value that needs to be

evaluated before it's thrown.


One sore point is that some people strongly oppose a Show instance like this. This is an open discussion,

but for now I believe we need to make the tradeoff at this point in

the spectrum. The displayException method in the Exception typeclass may allow for a better resolution to this point in the future.


Why GHC's point in the design space is great

This section is adapted from a comment I made on Reddit in 2014.

I don't believe there is a better solution to sync

exceptions, actually. That's because most of the time I see people

complaining about IO throwing exceptions, what they really mean is "this specific exception just bit me, why

isn't this exception explicit in the type signature?" To clarify my

point further:


  • There are virtually 0 IO actions that can't fail for some reason.

  • If every IO action returned a IO (Either UniqueExceptionType a), the programming model would become incredibly tedious. * Also, it would become very likely that when a is (), it would be easy to forget to check the return type to see if an exception occurred.

  • If instead every IO action returned IO (Either SomeException a), we'd at least not have to deal

    with wrangling different exception types, and could use

    ErrorT to make our code simpler, but...

  • Then we've just reinvented exactly what IO does today, only less efficiently!

My belief is that people are simply ignoring the reality of the situation: the contract for IO implicitly includes

"this action may also fail." And I mean in every single case. Built

in, runtime exceptions hide that in the type, but you need to be

aware of it. Runtime exceptions also happen to be far more efficient than using ErrorT everywhere.


And as much as some people complain that exceptions are difficult to handle correctly, I highly doubt ErrorT

or anything else would be easier to work with, we'd just be trading

in a well-developed, mostly-understood system for a system we think

we understand.


Concrete example: readLine

After a request on Twitter, I decided to add a little section here showing a

pragmatic example: how should we implement a function to read a

line from stdin and parse it? Let's start with a simpler question:

how about just parsing an input String? We'd like to

have a meaningful exception that tells us which value didn't parse

(the input String) and what type we tried to parse it as. We'll implement this as a readM function:


This meets our criteria of having a generalizable function to

multiple monads and useful exceptions. If we now make a

readLine function that reads from stdin, we have essentially two different choices of type signature:


  • readLine1 :: (MonadIO m, MonadThrow n, Read a, Typeable a) => m (n a): With this signature, we're saying "the

    case of failure is pretty common, and we therefore don't want to

    mix it in with the same monad that's handling IO side-effects.

  • readLine2 :: (MonadIO m, MonadThrow m, Read a, Typeable a) => m a: By contrast, we can actually combine the two different monads (IO side-effects and failure) into

    one layer. This is implicitly saying "failure is a case we don't

    usually want to deal with, and therefore the user should explicitly

    use tryAny or similar to extract such failures. That

    said, in practice, there's not much point in having both

    MonadIO and MonadThrow, since you can just use liftIO to combine them (as you'll see in a moment). So instead, our signature is readLine2 :: (MonadIO m, Read a, Typeable a) => m a.

Which one of these you choose in practice really does depend on

your personal preferences. The former is much more explicit about

the failure. However, in general I'd steer away from it, since -

like ExceptT over IO - it gives the false

impression that all failures are captured by the inner value.

Still, I thought it was worth demonstrating both options:


See also

Like what you learned here? Please check out the rest of our

Haskell Syllabus or learn about

FP Complete training.