Exceptions tutorial
Published on March 3, 2019, last updated October 31, 2021
This text originally was written as a chapter for the Intermediate Haskell book. Due to lack of progress with the book in the last year, other authors agreed to let me publish the text as a standalone tutorial so that people can benefit at least from this part of our work.
- The motivation for exceptions
- Throwing exceptions
- Catching exceptions
-
throwIO
- The implementation of throwing and catching
- Defining the hierarchy of exceptions
- Asynchronous exceptions
- Lifting the exception-related functionality
- How to avoid catching asynchronous exceptions
The motivation for exceptions
It is not uncommon to hear the opinion that exceptions are ugly and hard to work with. Especially in a language like Haskell, with its strong and powerful type system, perfectly capable of expressing exceptional situations in a pure way (i.e. in simplest case as a sum type containing both values that belong to normal results and values that represent adverse outcomes), why should we “contaminate” such a language with the concept of exceptions?
The topic itself is a bit controversial in the community:
-
Some believe that once you start dealing with exceptions, you should go all the way with them and not to pretend that your code is “safe” from exceptions by adding additional
MaybeT
orExceptT
layers. -
Others are concerned with purity and try to “disinfect” their APIs from exceptions completely (see, for example, the description of the
hasql
package, which claims that “the API comes free from all kinds of exceptions”). -
The third opinion is that exceptions, as the name suggests, are for exceptional situations, while explicit encoding of adverse outcomes in the type of returned value is for the situations that are expected to be quite common, and so an explicit handling is desirable.
To see the motivation for introducing exceptions in Haskell, let us consider a simple arithmetic expression:
percent :: Double -> Double -> Double
percent x y = (x / y) * 100
This does look conventional, but the (/)
operator is not total! As we know
well, one does not just divide by zero. So the whole percent
function has
a “morally wrong” type, or at least a type that does not reflect an
important part of percent
‘s semantics. We must note that it is still
convenient to have percent
in this form.
If we went with the approach of purists, we could end up writing programs like this:
percent :: Double -> Double -> Maybe Double
percent x y = (* 100) <$> (x /? y)
where
a /? 0 = Nothing
a /? b = Just (a / b)
While it does not look that bad in this little example, we would quickly
find ourselves writing all our code in Maybe
or Either
monads and the
corresponding transformers. Either SomeError
monad in particular would
have to account (in SomeError
) for an enormous number of things that can
go wrong. In a real-world program, there are a lot of failure scenarios that
are indeed rather “exceptional” and normally do not happen. SomeError
also
would need to be extendable somehow because combining code from different
libraries A and B would mean building a sum type capable of representing
exceptional situations of both A and B. It is somewhat inconvenient to code
that way.
What to do? Language design is often about balancing different arguments and considerations. In our case, we balance ease of use with correctness. By “correctness” here we mean explicit inclusion of adverse conditions in the types.
Precise semantics of exception throwing and handling will be given in the next sections, but for now let’s try to define the common properties that exceptions have in many programming languages:
-
They shortcut execution and propagate. This is handy, because once something exceptional has happened, our confidence in success of the program should be realistically very low, so we want to shut it down.
-
They can be caught. This follows the philosophy make common things easy and more tricky things possible. If an exception can be caught, it becomes not just a way to abort the entire program, it becomes a means to control program’s execution flow opening the possibility to recover from an exceptional situation, not less powerful than explicit handling with something like the
ExceptT
monad transformer.
Thus, whether to return a result wrapped in Maybe
, Either
, or to throw
an exception should be decided on how common the failure in question is and
how much attention we want to draw to it. If the failure is common, it makes
sense to force the consumer of the API to handle it in all cases for their
own good (by wrapping in Maybe
, Either
, ExceptT
, etc.). Otherwise, the
exceptional case should be covered by throwing an exception, which works a
lot like an implicit short-circuiting monad, except it is neither visible
nor the programmer is forced to think about it until they need to.
Other reasons to prefer throwing exceptions are:
-
preserving laziness, as we do not need to constantly check whether we have run into a failure and should short-circuit the execution;
-
more efficient code (the fact that the code that throws exceptions does not make it to run slower).
Finally, sometimes it is necessary or convenient to make some function
partial and signal an error that would indicate that the programmer made a
mistake. We try hard to avoid these cases in Haskell, but sometimes one has
to bite the bullet. In that case, exceptions allow us to signal the error
using functions like error
and assert
.
Throwing exceptions
Recall from the first example that it should be possible to throw exceptions from pure code. Given that Haskell has non-strict semantics where evaluation happens on demand (i.e. in order that is generally hard to predict), and that throwing an exception actually changes control flow dramatically, we seem to be in a tricky position immediately because of the possible non-determinism.
Consider the following example:
x :: Integer
x = error "foo" + error "bar"
When x
is evaluated, what will be thrown, the exception from error "foo"
or error "bar"
? It is not possible to tell, since the order in which (+)
evaluates its arguments is not defined.
Programs in Haskell often do not have predictable evaluation order, so the only way to reason about them is to consider values we want to compute. Indeed, what is an exception as not an additional kind of value that inhabits all types?
Let’s take a look at the signature of the throw
function and compare it
with bottom, shown here in the form of the undefined
function:
throw :: Exception e => e -> a
undefined :: a
We can see that throw
can lift an instance of the Exception
type class
(more about the type class later) into any value, just like
undefined
—bottom value—is a value found in any lifted type. It is
generally well-known that all lifted Haskell values are inhabited by bottom,
but in the presence of exceptions, it is also true that exceptions inhabit
any type.
Catching exceptions
If exceptions in Haskell could not be caught, we could consider them bottom
values and the non-determinism throw
introduces would be not that bad, but
once we equip the language with a way to catch exceptions, they can
influence the behavior of our programs, and if exceptions are
non-deterministic, our programs become non-deterministic as well. Not fun!
Let’s take an example from the paper A semantics for imprecise exceptions:
let x = (1/0) + (error "Urk")
in getException x == getException x
Here, getException :: a -> Either Exception a
is a hypothetical function
that allows us to catch exceptions in pure code. What this expression will
return, True
or False
? It would seem natural to state that the result
should be True
, but it could be False
just as well because of the
non-determinism (i.e. the two occurrences of x
could be replaced by x
‘s
definition, and (+)
could evaluate its left or right argument first).
One solution the above-mentioned paper proposes is to return the full set of
exceptions in every case, so getException x == getException x
evaluates to
True
reliably. That is not efficient, however, as once an exception is
thrown, we would need to force the entire expression to collect all other
exceptions no matter what.
In reality, there is no good way to make throwing/catching of exceptions in
a non-strict language deterministic, so the next best thing we can do is to
admit that getException
cannot be deterministic in pure code, and so, we
have to put it in IO
.
With getException :: a -> IO (Either Exception a)
, we can write:
main :: IO ()
main = do
let x = (1/0) + (error "Urk")
v1 <- getException x
v2 <- getException x
print (v1 == v2)
This still can print True
or False
, but now it is nothing to worry
about, because we are in the IO
monad, which depends on the state of
RealWorld
.
The Control.Exception
module features many functions that are useful for
dealing with exceptions. One such function is throw
(which we already
know) yet there is no getException
. But there is catch
:
catch :: Exception e
=> IO a -- ^ The computation to run
-> (e -> IO a) -- ^ Handler to invoke if an exception is raised
-> IO a
It is quite similar to getException
in that it also lives in IO
.
However, the first argument (the computation to catch exceptions from) also
lives in the IO
monad. That is because IO a
is a more common case as we
will see in the next section.
throwIO
While it is somewhat hard to predict when throw
will raise an exception in
pure code, there is no problem with raising an exception from the IO
monad
(or any IO
-enabled monadic stack) at a certain point of execution.
The IO
monad imposes an ordering on execution by being a state monad that
passes around a magical value that is never looked at, but creates logical
dependencies between separate IO
actions that are bound together with the
bind operator (>>=)
. This way it is possible to have throwIO :: Exception e => e -> IO a
, which will throw exactly when it is executed:
main :: IO ()
main = do
foo
throwIO myException
bar
Here myException
will be thrown after foo
but before bar
.
We must note the crucial difference between throw e :: a
and throwIO e :: IO a
, which is exactly the difference between evaluation and execution:
evaluation triggers exception in the case of throw e
, while execution
(when the magical state “goes through” throwIO
) triggers exception in the
case of throwIO e
. This is best demonstrated by the example with seq
found in the docs of throwIO
:
throw e `seq` x {- => -} throw e -- throwing triggered by evaluation
throwIO e `seq` x {- => -} x -- throwing triggered by execution
e `seq` x
is a function that artificially creates a dependency between
e
and x
, then returns x
. Evaluation of x
requires evaluation of e
as if its value directly depended on it. Evaluation forced by seq
does not
trigger exception in the case of throwIO
, because the expression is not
executed.
throwIO
, being much more predictable (execution order is more predictable
than evaluation order in Haskell), is generally preferred in the community
and it is a good idea to throw all your exceptions using throwIO
instead
of throw
.
The implementation of throwing and catching
A desirable property of exceptions is that computing a value with an
exceptional component does not make program slower or otherwise
less-efficient. Unlike explicit bookkeeping with Maybe
, Either
, or
similar, if an exception is not raised, it is the same as if there is no
exception at all.
Here is how synchronous exceptions are implemented:
-
getException
(orcatch
) marks the evaluation stack and evaluates its argument to weak head normal form. -
When an exception is raised, the stack is trimmed to the top-most
getException
mark. After that a handler can be run or a value indicating that an exception has happened can be returned. -
If evaluation has completed without raising an exception, the computed value can be returned.
-
If a pure expression throws, its thunk is replaced by the exception because we can be sure that it will throw every time we try to evaluate the thunk.
This section is worth ending with the following quote from A semantics for imprecise exceptions:
Notice that an exceptional value behaves as a first class value, but it is never explicitly represented as such. When an exception occurs, instead of building a value that represents it, we look for the exception handler right away. The semantic model (exceptional values) is quite different from the implementation (evaluation stacks and stack trimming). The situation is similar to that with lazy evaluation: a value may behave as an infinite list, but it is certainly never explicitly represented as such.
Defining the hierarchy of exceptions
Let’s consider how Haskell keeps its exceptions extendable. It is natural to think about exceptions as objects in a hierarchy:
It is well-known that in the object-oriented world the set of operations is fixed, while the set of objects is extendable. In functional programming the situation is reversed: typically the data types are fixed, and the set of functions on these objects is extendable. This works pretty well most of the time, and is especially natural in certain domains, like compilers, but now that we would like to build a hierarchy for exceptions, the functional solution is going to look somewhat… stylized.
To have an extensible group of data types that share a certain property,
Haskell has type classes. We have already seen that throw
has the
signature that features the mysterious Exception
constraint:
throw :: Exception e => e -> a
Anything that is an instance of Exception
can be thrown. Let’s take a look
at the type class:
class (Typeable e, Show e) => Exception e where
toException :: e -> SomeException
toException = SomeException
fromException :: SomeException -> Maybe e
fromException (SomeException e) = cast e
displayException :: e -> String
displayException = show
First of all, an Exception
needs to be printable (in case it bubbles to
the top level and terminates a program), hence Show
is a superclass of
Exception
. displayException
(which was added later) allows us to define
a human-readable representation of exception, which is by default
show
-based.
Of course, the interesting part of the definition here is the toException
and fromException
methods: they provide a way to inject and project an
Exception
to/from SomeException
.
And here is how throw
itself implemented in base
:
throw :: Exception e => e -> a
throw e = raise# (toException e)
This raise# :: b -> o
primitive can in principle throw anything at all,
but using it directly would lead to a chaos because there would be no way to
select a branch of the exception hierarchy, or to select all possible
exceptions.
SomeException
, being the root of our hierarchy, defined as:
data SomeException = forall e. Exception e => SomeException e
It is an existential wrapper around instances of
Exception
. It hides information about the type e
stored inside
SomeException
. The only thing we know about e
is that it is an instance
of Exception
.
The throwing part should be clear now, but what about catching? We want to
be able to say “I want to catch only arithmetic exceptions, like division by
zero”. Here is where fromException
comes into play. The default
implementation does everything we need: it unwraps SomeException
, then
tries to cast
the inner value to the desired type e
. If we have got
something inside Just
, the exception is of the correct type and we can do
something with it, otherwise we just re-throw it. This is what happens
inside of catch
and similar functions:
catch :: Exception e
=> IO a -- ^ The computation to run
-> (e -> IO a) -- ^ Handler to invoke if an exception is raised
-> IO a
catch (IO io) handler = IO $ catch# io handler'
where
handler' e =
case fromException e of
Just e' -> unIO (handler e')
Nothing -> raiseIO# e
If we try to call catch
(or other general exception-catching function)
without adding any information about the type of exception to catch, we will
get an ambiguous type error.
cast
can be used only with instances of Typeable
, which is why it is a
superclass of the Exception
type class. The Typeable
type class,
derivable by the compiler with the DeriveTypeable
language extension
(which is not necessary with the newer versions of GHC), allows us to get
type of a value at runtime. We will not go into the details here, but
suffice it to say that knowing the type of a value we can check if it is the
same as the type we want to convert to, so we can go from the abstract e
to a concrete type.
The fromException
method works with SomeException
just fine, it never
fails (so when we catch SomeException
, we catch every exception possible):
instance Exception SomeException where
-- …
fromException = Just
Right now we have a hierarchy with two levels, SomeException
as the root,
and all instances of Exception
as its immediate children. It is however
quite simple to add another existential wrapper between SomeException
and
concrete instance of Exception
with the aim of selecting a subset of all
exceptions that are wrapped with that wrapper.
Let’s pick the SomeAsyncException
from base
as an example of such a
wrapper. It is defined in the same fashion as SomeException
:
data SomeAsyncException = forall e. Exception e => SomeAsyncException e
There is nothing unusual in SomeAsyncException
‘s Exception
instance, it
uses the default definitions. If we take a look how Exception
instance is
defined for AsyncException
—data type for async-specific exception like
stack overflow—we can see the following:
instance Exception AsyncException where
toException = toException . SomeAsyncException
fromException x = do
SomeAsyncException a <- fromException x
cast a
toException
simply wraps AsyncException
in SomeAsyncException
before
calling toException
again (now the Exception
instance of
SomeAsyncException
is used), which in turn wraps the whole thing in
SomeException
.
fromException
first unwraps x :: SomeException
with fromException
(again the instance of SomeAsyncException
is at work here), the result is
returned in Maybe
, so the do
block is in the Maybe
monad here. If we
were lucky enough to extract SomeAsyncException
, we can try to cast
it
to get to the ArithException
type.
If we define another exception data type with a similar instance definition,
we will be able to catch it and AsyncException
at the same time by
specifying just SomeAsyncException
as the type of exception to catch. We
have a working hierarchy for our exceptions now.
This system of exception organization was proposed in the paper An extensible Dynamically-Typed Hierarchy of Exceptions.
Asynchronous exceptions
So far we have been talking about synchronous exceptions, that is, exceptions that affect the same thread where evaluation of exceptional value takes place. Asynchronous exceptions are initiated outside of the thread they affect. Haskell supports fully-asynchronous exceptions that allow us to:
-
interrupt the program from the outside (for example when the user presses Ctrl+C in a terminal);
-
receive signals about exceptional conditions that relate to resource consumption: stack overflow, reaching of allocations limits, heap overflow, etc.;
-
kill threads that are recognized as permanently blocked by the Haskell’s run time system;
-
control execution in a multi-threaded environment allowing one thread to throw exceptions to another thread to terminate the latter (used to implement timeouts and other multi-thread control idioms).
I used the phrase “fully-asynchronous exceptions”, but what exactly does it mean?
There are different ways to implement asynchronous exceptions. One way (preferred in the imperative world), is to employ semi-asynchronous techniques where the thread that initiates an exception sets a flag, which is then periodically checked by the thread that is to receive the exception. This however cannot work in Haskell, as we should be able to interrupt evaluation of pure code, but polling of a global flag would certainly be a side-effect.
In the volatile, mutable world of imperative programming, it is just too dangerous to allow interruption at an arbitrary moment in time. In contrast to that, there is absolutely no danger in terminating evaluation of pure code, because it does not perform any mutations or side-effects. That means that all pure code works fine with fully-asynchronous exceptions without change. We should note that this model also has higher modularity because the target thread does not need to do anything in order to be able to receive asynchronous exceptions.
Speaking of semantics of values being evaluated in the presence of asynchronous exceptions, we can say that since the exceptions can strike at any time during evaluation of any value in our program, we cannot consider asynchronous exceptions as part of semantics of value that is being evaluated when such an exception happens.
Masking asynchronous exceptions
Oftentimes it is necessary to ensure a certain level of atomicity so that asynchronous exceptions do not strike in the middle of a composite operation leading to a partially updated, inconsistent state of program.
To illustrate this, consider a toy example—the updateVar
function, which
applies a function to the contents of a mutable variable:
updateVar :: MVar a -> (a -> IO a) -> IO ()
updateVar m f = do
x <- takeMVar m -- (1)
x' <- f x -- (2)
putMVar m x' -- (3)
A quick reminder, MVar
s work like this:
-
MVar a
can be either empty or have a value of typea
inside. -
takeMVar
extracts a value from given mutable location if it is full, otherwise it blocks waiting for the value to be placed there. -
putMVar
places a value in the specified mutable location. If it is already full, it blocks and waits for the location to become empty.
In the current form, updateVar
is not an atomic operation, because
asynchronous exception can strike between (1) and (3), during evaluation of
f x
(2) leaving m
empty. To prevent asynchronous exceptions from
interrupting execution of updateVar
, we can use mask
:
-- | blocking callback |
mask :: ((forall a. IO a -> IO a) -> IO b) -> IO b
-- | unblocking callback |
It probably looks a bit confusing, so let me explain. The argument of mask
is the function to run with asynchronous exceptions disabled or “masked” (or
rather delayed). That function in turn receives another callback that allows
us to unmask asynchronous exceptions.
On a lower level, there are wrappers like block :: IO a -> IO a
and
unblock :: IO a -> IO a
that modify the masking state of a thread while
the inner code in executed. When block
and unblock
are nested, only the
innermost layer matters:
-- async exceptions:
block (block io) -- io blocked
block (unblock io) -- io unblocked
unblock (unblock io) -- io unblocked
unblock (block io) -- io blocked
The masking state of the current thread can be looked up with the
getMaskingState :: IO MaskingState
function. The MaskingState
type is
the enumeration of all possible masking states:
getMaskingState :: IO MaskingState
data MaskingState
= Unmasked -- ^ Thread is not inside mask or uninterruptibleMask
| MaskedInterruptible -- ^ Thread is inside mask.
| MaskedUninterruptible -- ^ Thread is inside uninterruptibleMask.
mask
is defined like this:
mask :: ((forall a. IO a -> IO a) -> IO b) -> IO b
mask io = do
b <- getMaskingState
case b of
Unmasked -> block $ io unblock
MaskedInterruptible -> io block
MaskedUninterruptible -> io blockUninterruptible
The definition gives us a hint about the behavior of nested mask
s. We can
see that the inner “unblocking” callback does not necessarily unmask
asynchronous exceptions, but rather returns masking state to what it was
outside of the current mask
. Ignore blockUninterruptible
for now, we
will get to it soon.
Let’s continue with the examples:
mask (\_ -> mask (\_ -> io)) -- io blocked
mask (\_ -> mask (\restore -> restore io)) -- io blocked
Here, the first mask
layer blocks asynchronous exceptions and the second
mask
layer can not unblock with restore
because that callback only
resets the masking state to what it was outside of the respective mask
call. The behavior is exactly what we usually want: when we enclose a region
of code with mask
, we want to be sure async exceptions are masked, no
matter what happens inside.
The guarantee extends even further: mask
affects all code that is
lexically enclosed by it, even if we fork with forkIO
and forkOS
(the
new thread inherits the parental masking state). This is important because
otherwise there would be no way to prevent exceptions happening between (1)
and (2) in the following example:
x <- takeMVar m
forkIO $ -- (1)
mask $ \unmask -> do -- (2)
x' <- catch (unmask (…))
(\e -> putMVar m x >> throw (e :: SomeException))
putMVar m x'
There is also mask_
in Control.Exception
for the cases when we only want
to block:
mask_ :: IO a -> IO a
mask_ io = mask $ \_ -> io
Let’s use mask
in our example to make it more correct in the presence of
asynchronous exceptions:
updateVar :: MVar a -> (a -> IO a) -> IO ()
updateVar m f = mask $ \unmask -> do
x <- takeMVar m -- (1)
x' <- unmask (f x) -- (2)
putMVar m x' -- (3)
This is a bit better in the sense that asynchronous exceptions can no longer
strike between (1) and (2), (2) and (3), but they still can happen on the
line (2), while f x
is being evaluated.
There are two possible ways to fix it:
-
Run
f x
without unmasking, wrapping the whole thing withmask_
. This solution has a flaw however: iff x
takes a very long time to compute or worse yet, hangs, there is no way to abort the thread. -
Add
catch
to handle exceptions thrown fromf x
and so re-fillm
with the old value should we be forced to abort the computation.
Let us explore the solution involving catch
:
updateVar :: MVar a -> (a -> IO a) -> IO ()
updateVar m f = mask $ \unmask -> do
x <- takeMVar m
x' <- catch (unmask (f x))
(\e -> putMVar m x >> throw (e :: SomeException))
putMVar m x'
This code behaves as expected: the value inside m
is guaranteed to be
updated and if an asynchronous exception strikes while we are in the unmasked
state old value will be preserved.
Now here comes a subtle detail about masking. It is explained in papers and in the Simon Marlow’s book Parallel and Concurrent Programming in Haskell, but we will try to re-iterate it here as concisely and clearly as possible:
-
Blocking (for example while waiting for a value to be put into
m
, as intakeMVar m
) with asynchronous exceptions disabled is a bad thing. This increases the probability of deadlock that cannot be interrupted (normally some deadlocks are detected by the Haskell’s runtime system and an exception such asBlockedIndefinitelyOnMVar
is thrown to blocking threads to shut them down). -
To deal with that, certain operations like
takeMVar
were made interruptible, that is, themask
can be “pierced” and an asynchronous exception can get through it. But they are only interruptible when they actually block, for example whentakeMVar m
is waiting form
to be filled with a value (otherwisemask
would be meaningless).
Let’s consider various scenarios of what might happen:
-
When
takeMVar m
is blocking, we can throw an asynchronous exception to the thread we are working in and it will be treated in fact as a synchronous exception happening exactly beforetakeMVar m
. The thread will be killed. Asynchronous exceptions become synchronous insidemask
, because it is known which operations are interruptible and so the exception becomes synchronized with respect to other operations in the thread. -
putMVar
is also interruptible, because it can block (waiting for its argument to become empty). Does it mean that an asynchronous exception can interrupt either of ourputMVar
s from the example above? No, it can not. We can see from the example thatm
is guaranteed to be empty after the lasttakeMVar
invocation, soputMVar
s cannot be interrupted in that case, as it does not need to wait form
to become empty.
As we can see, the tricky part in the behavior of so-called “interruptible” operations is that they only become interruptible when they actually block. All operations that may block indefinitely are designated as interruptible.
Uninterruptible masking
Now that we know that masking asynchronous exceptions with mask
can be
pierced, we also need to discuss a way to mask in an uninterruptible fashion
and when it is useful.
Similar to mask
, there is uninterruptibleMask
, which uses
blockUninterruptible
instead of block
. It should be noted that the
ability to interrupt operations is there for a good reason: if your program
hangs inside of an uninterruptible mask, it will become unresponsive and
there will be no way to kill it.
That said, sometimes uninterruptible mask is useful. To show an example of
that, let us first introduce the bracket
function:
bracket
:: IO a -- ^ Computation to run first (acquire resource)
-> (a -> IO b) -- ^ Computation to run last (release resource)
-> (a -> IO c) -- ^ Computation to run in-between
-> IO c -- ^ Returns the value from the in-between computation
bracket before after thing =
mask $ \restore -> do
a <- before
r <- restore (thing a) `onException` after a
_ <- after a
return r
Everything in bracket
happens with asynchronous exceptions masked, except
for thing a
.
onException :: IO a -> IO b -> IO a
is another useful function from the
Control.Exception
module. f `onException` g
tries to run f
, but if
it throws an exception, onException
runs g
and re-throws:
onException :: IO a -> IO b -> IO a
onException f g = f `catch` \e -> do
_ <- g
throwIO (e :: SomeException)
bracket
allows us to guarantee that some resource will be released no
matter what. However, there are a number of things that can invalidate this
promise. For now we will consider one particular use case—working with
temporary directories:
withTempDirectory
:: FilePath -- ^ Directory in which to create temp directory
-> String -- ^ Name pattern for the temp directory
-> (FilePath -> IO a) -- ^ Callback that receives the name of temp directory
-> IO a -- ^ Result returned from the callback
withTempDirectory targetDir template = bracket
(createTempDirectory targetDir template)
(ignoringIOErrors . removeDirectoryRecursive)
The failure scenario is taken from this blog post by Roman Cheplyaka:
-
createTempDirectory targetDir template
completes successfully. -
User’s action completes successfully creating some files in the temporary directory (the third argument of
withTempDirectory
, it is not bound explicitly because of eta-reduction). -
Clean up
removeDirectoryRecursive
begins, but while it is working an asynchronous exception is received. Even though it is insidemask
(it isafter a
in the definition ofbracket
), individual file deletions are interruptible (because they may block indefinitely), so the mask gets pierced. Result: the guarantees ofwithTempDirectory
are broken.
One solution is to use something like this:
withTempDirectory'
:: FilePath
-> String
-> (FilePath -> IO a)
-> IO a
withTempDirectory targetDir template action =
uninterruptibleMask $ \restore -> do
tdir <- createTempDirectory targetDir template
let after = ignoringIOErrors . removeDirectoryRecursive
r <- restore (action tdir) `onException` after tdir
after tdir
return r
uninterruptibleMask
makes sure that creation of temporary directory and
its deletion will not be interrupted.
Use uninterruptibleMask
only when you know for sure that its inner
computation will not ever block for a long period of time. Chances are, most of
the time you will not need uninterruptibleMask
, but it is good to know about
it and have it in your programming toolbox.
The functions that guarantee release of resources in case of exception,
such as bracket
, may also let you down for reasons that are not related to
asynchronous exceptions. It is important to remember that when the main
thread of program finishes, the program exits instantly, without sending
asynchronous exceptions to child threads and so without giving them a chance
to clean up properly. If the program in question uses bracket
(or similar
function) to manipulate internal data (such as an MVar
), it is OK.
However, if it manipulates an object from the outside (for example, creates
a temporary directory and then deletes it), it is only guaranteed to clean
up properly if it is run in the main thread. The most natural solution to
this is not to fork manually, but with withAsync
from the async
package. That function will ensure that the forked thread is killed when the
inner computation returns or throws.
The throwTo
function
throwTo
allows us to raise an arbitrary exception in a thread with a known
ThreadId
:
throwTo :: Exception e => ThreadId -> e -> IO ()
The docs for throwTo
are exceptionally good, so let us just quote them
here with some clarifications:
-
Exception delivery synchronizes between the source and the target thread:
throwTo
does not return until the exception has been raised in the target thread. The calling thread can thus be certain that the target thread has received the exception. Exception delivery is also atomic with respect to other exceptions. Atomicity is a useful property to have when dealing with race conditions: e.g. if there are two threads that can kill each other, it is guaranteed that only one of the threads will get to kill the other. -
This means in particular that use of
mask
can block the thread that attempts to throw an exception to a thread in masked state. Like any blocking operation,throwTo
is therefore interruptible, but unlike other interruptible operations, however,throwTo
is always interruptible, even if it does not actually block. -
If the target thread is currently making a foreign call, then the exception will not be raised (and hence
throwTo
will not return) until the call has completed. This is the case regardless of whether the call is inside a mask or not. However, in GHC a foreign call can be annotated as interruptible, in which case athrowTo
will cause the RTS to attempt to cause the call to return. -
There is no guarantee that the exception will be delivered promptly, although the runtime will endeavor to ensure that arbitrary delays do not occur. In GHC, an exception can only be raised when a thread reaches a safe point, where a safe point is where memory allocation occurs. Some loops do not perform any memory allocation inside the loop and therefore cannot be interrupted by a
throwTo
. -
Whatever work the target thread was doing when the exception was raised is not lost: the computation is suspended until required by another thread. This is best understood if we imagine an expensive pure computation that is interrupted by an asynchronous exception—what we have evaluated so far is not lost.
-
If the target of
throwTo
is the calling thread, then the behavior is the same asthrowIO
, except that the exception is thrown as an asynchronous exception. This means that if there is an enclosing pure computation, which would be the case if the current IO operation is insideunsafePerformIO
orunsafeInterleaveIO
, that computation is not permanently replaced by the exception, but is suspended as if it had received an asynchronous exception. -
Note that if
throwTo
is called with the current thread as the target, the exception will be thrown even if the thread is currently insidemask
oruninterruptibleMask
.
This are certainly a lot of subtle points to keep in mind. The main takeaway
is that throwTo
is synchronized with the thread it throws to (i.e. it does
not return till the exception has been raised) and so it may block if the
target thread is in mask
or doing a foreign call.
How asynchronous exceptions are implemented
Information on implementation of asynchronous exceptions can be found in the paper Asynchronous exceptions in Haskell. Here we just briefly enumerate the main points for curious readers:
-
Every thread has a data block associated with it to store thread-specific data. The data includes the masking state we have discussed and a queue of asynchronous exceptions pending delivery.
-
When thread is not in masked state, the queue of asynchronous exceptions is checked at regular intervals and if there are exceptions pending, they are delivered.
-
When thread’s masking state goes from “masked” to “unmasked”, the queue is checked right away instead of waiting for the next check.
-
When
getException
orcatch
marks the evaluation stack, it also saves current masking state so it can be restored after handling an exception. -
Two more marks (or “frames” in the terminology of the above-mentioned paper) are necessary: one for
block
and another one forunblock
. When execution passes either of these, masking state changes accordingly. There are also some rules for keeping the evaluation stack from growing unnecessarily, but we will not include them here. -
throwTo
simply places an exception in the queue of target thread then blocks till the exception is delivered.
Lifting the exception-related functionality
The functions from the Control.Exception
module (which we advise to
examine on your own too, it is well documented) cover all needs we might
have while dealing with exceptions. However, they only work in the IO
monad. That is a sane choice for a module in the base
package as lifting
of these functions into arbitrary monad stacks is not always
straightforward, and base
cannot depend on transformers
.
In this section, we are going to look at the exceptions
package first. Then we will consider a somewhat more flexible but
lower-level alternative in the form of the monad-control
package. Finally, we will examine the newer unliftio
package.
Lifting with exceptions
The exceptions
package provides three principal type classes:
-
MonadThrow
for monads that support an analogue ofthrowIO
. -
MonadCatch
for monads that provide a liftedcatch
. -
MonadMask
for monads that provide liftedmask
anduninterruptibleMask
functions.
throwM
of MonadThrow
is defined just as lifted throwIO
, except for
pure monads, for which it just returns an “empty” value (which in monadic
setting also shortcuts execution):
class Monad m => MonadThrow m where
throwM :: Exception e => e -> m a
instance MonadThrow [] where
throwM _ = []
instance MonadThrow Maybe where
throwM _ = Nothing
instance MonadThrow IO where
throwM = Control.Exception.throwIO
instance MonadThrow m => MonadThrow (StateT s m) where
throwM = lift . throwM
-- etc.
Indeed, there is generally no problem with throwing exceptions, so it is rather trivial to define such a class. Arguably, the ability to throw in pure setting is a good thing, although the practice shows that industrial users are mainly interested in lifting throwing functionality through monadic transformers.
Next, here goes MonadCatch
:
class MonadThrow m => MonadCatch m where
catch :: Exception e => m a -> (e -> m a) -> m a
instance MonadCatch IO where
catch = Control.Exception.catch
instance MonadCatch m => MonadCatch (StateT s m) where
catch = liftCatch catch
-- etc.
Not every monad that implements MonadThrow
is an instance of MonadCatch
.
For example Maybe
throws away the information about what went wrong, so
there cannot be a MonadCatch
instance for it.
On the other hand, if we try to convert something we caught to the expected
type and fail (fromException
returns Nothing
), we need to re-throw. This
is why MonadThrow
is a superclass of MonadCatch
.
What about the implementations? Well, the case of IO
looks trivial, as do
most others. The instance definitions for StateT
and other transformers
are more interesting though. We re-use here catch
defined for m
monad,
but lift it using liftCatch
, which is:
-- | Lift a @catchE@ operation to the new monad.
liftCatch :: Catch e m (a,s) -> Catch e (StateT s m) a
liftCatch catchE m h =
StateT $ \s -> runStateT m s `catchE` \e -> runStateT (h e) s
Where Catch
is just a type synonym:
type Catch e m a = m a -> (e -> m a) -> m a
From that, we can see that liftCatch
turns a catching function that
catches e
in monad m
which returns a value of type (a,s)
into a
catching function that catches the same exception e
in monad StateT s m
and returns just a
. This is the sort of code that is type-driven, meaning
we can write the implementation mostly by just following the types. If we
replace catchE
by catch
from Control.Exception
for simplicity and
assume m
fixed to IO
, the function starts to look more understandable:
-- | Lift a @catchE@ operation to the new monad.
liftCatch
:: Exception e
=> StateT s IO a -- ^ m, action to run
-> (e -> StateT s IO a) -- ^ h, exception handler
-> StateT s IO a
liftCatch m h =
StateT $ \s -> runStateT m s `catch` \e -> runStateT (h e) s
The result is in StateT
, so we start by putting its constructor in place,
and so we have the state s
. Both arguments of the “vanilla” catch
must
be in plain IO
, so we have to run m
and h
in order to unwrap them and
get to the IO
monad.
This shows that it is quite feasible to lift catch
into most monadic
stacks. Let’s see about MonadMask
.
class MonadCatch m => MonadMask m where
mask :: ((forall a. m a -> m a) -> m b) -> m b
uninterruptibleMask :: ((forall a. m a -> m a) -> m b) -> m b
instance MonadMask IO where
mask = Control.Exception.mask
uninterruptibleMask = Control.Exception.uninterruptibleMask
instance MonadMask m => MonadMask (StateT s m) where
mask a = StateT $ \s -> mask $ \u -> runStateT (a $ q u) s
where
q :: (m (a, s) -> m (a, s)) -> StateT s m a -> StateT s m a
q u (StateT b) = StateT (u . b)
uninterruptibleMask a =
StateT $ \s -> uninterruptibleMask $ \u -> runStateT (a $ q u) s
where
q :: (m (a, s) -> m (a, s)) -> StateT s m a -> StateT s m a
q u (LazyS.StateT b) = StateT (u . b)
-- etc.
The ability to receive/catch exceptions is a logical prerequisite for
masking of asynchronous exceptions, hence the MonadCatch
(and by
implication MonadThrow
) is a superclass of MonadMask
.
Let’s take a look at the case of StateT s m
again. mask
receives the a :: (forall a. m a -> m a) -> m b
argument which expects to get this forall a. m a -> m a
—unmasking callback working in the StateT s m
monad. Let’s
see how we provide that. First of all, the result of mask
must be in
StateT s m
, so we put the StateT
constructor in place and so we get the
state s
. Inside the lambda that binds s
we use mask
of underlying
monad m
, which we require to be an instance of MonadMask
. That mask
in
turn receives the unmasking callback u
(remember that the \u -> …
lambda
is in masked state, so its argument u
happens to be the unmasking
callback) which works in the inner monad m
, not StateT s m
. To feed this
unmasking callback into a
we lift it from m (a, s) -> m (a, s)
to
StateT s m a -> StateT s m a
with q
. Finally, a
applied to q u
is of
the type StateT s m
, but the inner mask
expects something in m
, so we
run it. The entire mask $ \u -> runStateT (a $ q u) s
thing thus ends up
being of the right type m (a, s)
to be put into StateT $ \s -> …
. Again,
this is an example of type-driven programming.
uninterruptibleMask
works exactly the same.
Having these there basic concepts (throwing, catching, and masking async
exceptions) abstracted in that way, the exceptions
package seems to be
well-equipped to define lifted versions of everything. For example, here is
how bracket
is defined:
bracket :: MonadMask m => m a -> (a -> m b) -> (a -> m c) -> m c
bracket acquire release use = mask $ \unmasked -> do
resource <- acquire
result <- unmasked (use resource) `onException` release resource -- (1)
_ <- release resource -- (2)
return result
It does make sense, although the generality of the lifting technique until
recently did not extend quite long enough to support all useful monads.
Also, when we deal with stateful monadic stacks, there may be several
different implementations of bracket
that differ in how the state is
passed around, and depending on your use-case, you may want one or another:
-
State from successful computation
use
affectsrelease
. Whenuse
fails,release
runs with the same state as were passed touse
. -
release
always runs with the same state asuse
. -
acquire
can/cannot affect state that is passed touse
. - Etc.
As an exercise, figure out how bracket
will pass around the state in the
case of StateT s m
monad transformer. Definitions of MonadCatch
and
MonadMask
instances for StateT s m
and bracket
are shown above. Hint:
concentrate on the difference between the lines (1) and (2). You will also
need the definition of lifted onException
from exceptions
:
onException :: MonadCatch m => m a -> m b -> m a
onException action handler = action `catchAll` \e -> handler >> throwM e
where
catchAll :: MonadCatch m => m a -> (SomeException -> m a) -> m a
catchAll = catch -- specialization of catch that catches all exceptions
Next, let’s consider ExceptT e m
as our monad. This monad transformer is
short-circuiting:
newtype ExceptT e m a = ExceptT (m (Either e a))
instance (Monad m) => Monad (ExceptT e m) where
return a = ExceptT $ return (Right a)
m >>= k = ExceptT $ do
a <- runExceptT m
case a of
Left e -> return (Left e)
Right x -> runExceptT (k x)
In English, if at some point (>>=)
gets m
with inner value inside
Left
, we finish immediately ignoring the computation k
altogether. This
is how monadic bind works for this transformer. Applying this to the
definition of bracket
we can see that if acquire
or use resource
happen to return something inside Left
, the later actions (including
release
) have no chance to run—guarantees of bracket
are broken.
Until recently MonadMask
had to rely on the guarantee that MonadCatch
catches all possible exceptions and there is no other way for the
computation to exit early—therefore, although there was a possible instance
of MonadCatch
for ExceptT
, it was not considered valid and so there was
no MonadMask
instance for ExceptT
.
Since version 0.9.0
of the exceptions
package, the problem is solved by
introducing a new method of the MonadMask
type class:
generalBracket
:: m a
-- ^ Acquire some resource
-> (a -> ExitCase b -> m c)
-- ^ Release the resource, observing the outcome of the inner action
-> (a -> m b)
-- ^ Inner action to perform with the resource
-> m (b, c)
where ExitCase
is:
data ExitCase a
= ExitCaseSuccess a -- ^ Success
| ExitCaseException SomeException -- ^ Exception thrown
| ExitCaseAbort -- ^ Aborting without exceptions, e.g. with Left
deriving Show
The solution is simple: for monads like ExceptT
we explicitly handle the
case when the inner computation exited by “aborting” computation (e.g. with
Left
result), not by throwing an exception. Then in the release handler we
can clean up in that case as well.
Functions like bracket
and finally
were previously defined using the
mask
method of MonadMask
, now they are defined via generalBracket
which allows them to work in monads like ExceptT
properly.
To learn how to write instances see the documentation for
generalBracket
on Hackage. The documentation is so good that it would be
pure duplication to try to add an explanation here as well.
Lifting with monad-control
The approach used in the monad-control package is to
temporarily “unlift” complex monadic stack to some base monad such as IO
so we can use the existing functions like catch
and bracket
from base
,
then “restore” the monadic stack when we are done.
To understand why we need to do unlifting instead of following a more
familiar path—lifting IO
computation into a more complex IO
-enabled
monadic stack—consider the catch
function:
catch :: Exception e
=> IO a -- ^ The computation to run
-> (e -> IO a) -- ^ Handler to invoke if an exception is raised
-> IO a
Even though we can use liftIO
for lifting after applying arguments to
catch
, it also expects IO a
as an argument, and here liftIO
cannot
help, in fact, we need the opposite. We need a way to unlift to IO a
. The
good news is that there is a way to unlift without losing information so we
can re-construct virtually any monadic stack built from the familiar
transformers.
To understand why it is so, look at the definitions of some monad transformers:
newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }
newtype StateT s m a = StateT { runStateT :: s -> m (a, s) }
newtype WriterT w m a = WriterT { runWriterT :: m (a, w) }
You probably have noticed the common structure inside the wrappers: m
containing what we will call state—information that can be used to
recreate the transformer. ReaderT
is stateless—its state is just the
monadic value a
(in ReaderT r m a
), while StateT
carries state s
,
and so its state is (a, s)
, similar to WriterT
. (The lambdas are not of
any concern for us here, as we can wrap anything with a lambda, it is not
part of the state.)
Since we cannot escape IO
, there is no IOT
monad transformer, and so if
there is IO
in monadic stack, it is always at the bottom. If we remove the
newtype
s, unlifting a complex monad transformer, we always get a value
that produces something like IO st
where st
is the state we mentioned
earlier. For example:
type MyStack r s w a = ReaderT r (StateT s (WriterT w IO)) a
{- isomorphic to -}
r -> s -> IO ((a, s), w)
Simply put, when transformers are stacked, their states are combined. With
the example shown, if we are currently in MyStack
, we have r
and s
to
apply and get IO ((a, s), w)
, which we can pass to a function such as
catch
.
The result of catch
, being of the type IO ((a, s), w)
can be wrapped
back into lambdas and newtype
s to the effect that we restore MyStack
monad back. This is the idea behind monad-control
. Now that the idea
should be clear, let’s see what form it takes in the actual library.
Meet the MonadBaseControl b m
type class which allows us to lift functions
that work in monad b
(like catch
, b ~ IO
) into a more complex monadic
stack m
:
class MonadBase b m => MonadBaseControl b m | m -> b where -- (1)
type StM m a :: * -- (2)
liftBaseWith :: (RunInBase m b -> b a) -> m a -- (3)
restoreM :: StM m a -> m a -- (4)
type RunInBase m b = forall a. m a -> b (StM m a) -- (5)
-
MonadBase
is a superclass ofMonadBaseControl
.MonadBase b m
is best understood as a generalization ofMonadIO
where we can lift arbitrary base monadb
(not justIO
) intom
.MonadBase
is a fairly trivial tool and will not be of interest in this chapter. Note thatMonadBaseControl
is a multi-parameter type class which has the functional dependencym -> b
. The functional dependency helps the compiler to resolve type ambiguity. It says: if you know whatm
instantiated to, then you can learnb
by searching existing instances for an instance with matchingm
. It is guaranteed by the compiler thatb
is uniquely identified by the choice ofm
. -
StM m a
is an associated type of the type classMonadBaseControl
. This feature is enabled by the-XTypeFamilies
language extension. This is the type of state we have talked about. -
liftBaseWith
is a function that takes another functionRunInBase m b -> b a
, inside which we generate a value in base monadb
. TheRunInBase m b
argument is the “running” or “unlifting” callback. Look at the definition (5), for a valuem a
this function strips all monadic layers till the desired base monadb
inside which we get the stateStM m a
. -
restoreM
allows us to restore/replace state in the monadm
from a value of the typeStM m a
. -
Type-synonym for the unlifiting callback, as explained in (3).
It is beneficial to learn how various instances of MonadBaseControl
are
defined. Let’s start from MonadBaseControl IO IO
:
instance MonadBaseControl IO IO where
type StM IO a = a
liftBaseWith f = f id
restoreM = return
This should make sense, to unlift IO a
we do not need to do anything.
Similarly, it is not difficult to restore this monad’s state because it has
none.
ReaderT
is a bit more interesting:
instance MonadBaseControl b m => MonadBaseControl b (ReaderT r m) where
type StM (ReaderT r m) a = StM m (StT t a)
liftBaseWith = defaultLiftBaseWith
restoreM = defaultRestoreM
Here we step into the territory of monad transformers. The definition builds
on the assumption that the inner monad m
in ReaderT r m
is also an
instance of MonadBaseControl b m
. To use that instance though, we first
need to go from ReaderT r m
to m
, i.e. we need to unlift a monad
transformer, ReaderT
. How do we do that?
The answer is: using the second important type class of
monad-control
—MonadTransControl
:
class MonadTrans t => MonadTransControl t where
type StT t a :: *
liftWith :: Monad m => (Run t -> m a) -> t m a -- like liftBaseWith
restoreT :: Monad m => m (StT t a) -> t m a -- like restoreM
type Run t = forall n b. Monad n => t n b -> n (StT t b)
MonadTransControl
to MonadTrans
is the same as MonadBaseControl
to
MonadBase
. MonadTrans
establishes a “connection” between monad
transformer t m
and its inner monad m
allowing to lift m
into t m
with lift
. Note that lift
lifts through only one monadic layer.
MonadTransControl
also allows us to unlift through one monadic layer.
MonadBase
, as we have mentioned already, allows us to lift through many
layers with a single liftBase
call, similarly liftBaseWith
lifts through
all layers till we reach the base monad.
Knowing this, let’s quickly go through the definitions of
defaultLiftBaseWith
and defaultRestoreM
:
defaultLiftBaseWith
:: (MonadTransControl t, MonadBaseControl b m)
=> (RunInBaseDefault t m b -> b a)
-> t m a
defaultLiftBaseWith f = liftWith $ \run ->
liftBaseWith $ \runInBase ->
f $ runInBase . run
defaultRestoreM
:: (MonadTransControl t, MonadBaseControl b m)
=> ComposeSt t m a
-> t m a
defaultRestoreM = restoreT . restoreM
defaultLiftBaseWith
unlifts through one monadic layer with liftWith
,
then we again call liftBaseWith
recursively which either happens to use a
terminal instance like MonadBaseControl IO IO
or another instance which
unlifts through the next monadic layer in exactly the same manner.
defaultRestoreM
restores the state in the base monad with restoreM
, then
lifts the base monad through one layer with restoreT
.
defaultLiftBaseWith
and defaultRestoreM
are used to implement not only
the ReaderT
instance, but also the StateT
and most other instances
because the actual transformer-specific logic is defined in the
MonadTransControl
instances:
instance MonadTransControl (ReaderT r) where
type StT (ReaderT r) a = a
liftWith f = ReaderT $ \r ->
f $ \t -> runReaderT t r
restoreT = ReaderT . const
instance MonadTransControl (StateT s) where
type StT (StateT s) a = (a, s)
liftWith f = StateT $ \s ->
liftM (\x -> (x, s)) (f $ \t -> runStateT t s)
restoreT = StateT . const
A common idiom with monad-control
is to return monadic state from the
function passed to liftBaseWith
:
RunInBase m b -> b (StM m a)
And then use restoreM
to immediately restore the monadic state. This is
captured by the control
helper:
control :: MonadBaseControl b m
=> (RunInBase m b -> b (StM m a))
-> m a
control f = liftBaseWith f >>= restoreM
Finally, let’s show how we can use bracket
from Control.Exception
with a
complex monadic stack:
liftedBracket
:: StateT s IO a -- ^ Computation to run first (acquire resource)
-> (a -> StateT s IO b) -- ^ Computation to run last (release resource)
-> (a -> StateT s IO c) -- ^ Computation to run in-between
-> StateT s IO c
liftedBracket acquire release use = control $ \runInBase ->
bracket
(fst <$> runInBase acquire) -- we dicard state s from (a, s)
(runInBase . release)
(runInBase . use)
Note that acquire
and release
cannot modify the state s
, it is
restored from the state returned from runInBase . use
. With a bit more
effort we could “fix” that:
liftedBracket'
:: StateT s IO a -- ^ Computation to run first (acquire resource)
-> (a -> StateT s IO b) -- ^ Computation to run last (release resource)
-> (a -> StateT s IO c) -- ^ Computation to run in-between
-> StateT s IO c
liftedBracket' acquire release use = control $ \runInBase ->
bracket
(runInBase acquire) -- returns (a, s)
(\(a, s) -> runInBase (put s >> release a))
(\(a, s) -> runInBase (put s >> use a))
State modifications made in acquire
now influence both restore
and
use
. The state from use
is restored, the state from restore
is
discarded (because there is no way to pick that b
value from the top level
signature). This shows that not only monad-control
allows us to do this
sort of lifting, it also allows us to control precisely what happens to the
monadic state.
Lifting with unliftio
The unliftio
package is a newer, simpler, and safer
alternative to monad-control
. It uses the same idea of unlifting monadic
stacks but it takes a bit different form:
class MonadIO m => MonadUnliftIO m where
-- | Capture the current monadic context, providing the ability to
-- run monadic actions in 'IO'.
askUnliftIO :: m (UnliftIO m)
askUnliftIO = withRunInIO (\run -> return (UnliftIO run))
-- | Convenience function for capturing the monadic context and running an 'IO'
-- action with a runner function. The runner function is used to run a monadic
-- action @m@ in @IO@.
withRunInIO :: ((forall a. m a -> IO a) -> IO b) -> m b
withRunInIO inner = askUnliftIO >>= \u -> liftIO (inner (unliftIO u))
-- | The ability to run any monadic action @m a@ as @IO a@.
--
-- This is more precisely a natural transformation. We need to new
-- datatype (instead of simply using a @forall@) due to lack of
-- support in GHC for impredicative types.
newtype UnliftIO m = UnliftIO { unliftIO :: forall a. m a -> IO a }
withRunInIO
is essentially the same thing as liftBaseWith
from
monad-control
, only specialized to IO
as the base monad (the most common
use case, if not the only). The UnliftIO
newtype is necessary to be able
to return a function that has universally quantified arguments (introduced
with forall
s) in its signature (this is the “impredicative polymorphism”
the documentation mentions). We only need this in askUnliftIO
. The method
is rather unique to unliftio
, it provides the running function forall a. m a -> IO a
that one can pass around freely and apply several times, to
different a
types.
The second difference between the library and monad-control
is that it
only defines instances of MonadUnliftIO
for stateless monadic stacks which
are isomorphic to ReaderT
over IO
. This way the question how to combine
state from different branches of computation does not arise.
How to avoid catching asynchronous exceptions
The last (but not least) issue we have to consider is the inability to tell
whether an exception we have caught was synchronous or asynchronous. When we
enclose our code with a function like catch
, it catches everything that
matches the exception type:
import Control.Concurrent
import Control.Concurrent.Async
import Control.Exception
catchErrorCall :: IO () -> IO ()
catchErrorCall m = catch m h
where
h :: ErrorCall -> IO ()
h e = putStrLn ("Caught: " ++ displayException e)
synchronous :: IO ()
synchronous = catchErrorCall $
throwIO (ErrorCallWithLocation "Synchronously thrown." "")
asynchronous :: IO ()
asynchronous = withAsync (catchErrorCall (threadDelay 1000000)) $ \a ->
throwTo (asyncThreadId a) (ErrorCallWithLocation "Asynchronously thrown." "")
λ> synchronous
Caught: Synchronously thrown.
λ> asynchronous
Caught: Asynchronously thrown.
It is often not an issue because normally we prefer to be very specific
about type of exception we want to catch. For example, if we want to catch
HttpException
(assume that it is an exception that an HTTP client library
throws when something goes wrong), it is very unlikely that someone will
throw it to our thread asynchronously.
Things start to get worse when we want to catch all exceptions, that is, we
specify SomeException
as the type of exception to catch. As we have
learned, asynchronous exceptions, like all other exceptions are converted to
SomeException
before being thrown, so by catching SomeException
we catch
both asynchronous and synchronous exceptions. This may lead to quite
unexpected results, because we probably want to safeguard against issues
that happen synchronously inside the code enclosed by catch
, not to catch
things like ThreadKilled
(which should just kill our thread).
One simple way to solve the problem is to assume that the exceptions that
are typically thrown asynchronously are wrapped in SomeAsyncException
(which is the case for all exceptions from Control.Exception
). Here is how
this could be done:
import Control.Concurrent
import Control.Concurrent.Async
import Control.Exception
catchOnlySync :: Exception e => IO a -> (e -> IO a) -> IO a
catchOnlySync = catchJust $ \e -> -- catchJust from Control.Exception
case fromException e of
Nothing -> fromException e
Just (SomeAsyncException _) -> Nothing
catchAllSync :: IO () -> IO ()
catchAllSync m = catchOnlySync m h
where
h :: SomeException -> IO ()
h e = putStrLn ("Caught: " ++ displayException e)
synchronous :: IO ()
synchronous = catchAllSync $
throwIO (ErrorCallWithLocation "A synchronous exception." "")
asynchronous :: IO ()
asynchronous = withAsync (catchAllSync action) $ \a ->
throwTo (asyncThreadId a) ThreadKilled
where
action = do
threadDelay 1000000
print "Boo!"
λ> synchronous
Caught: A synchronous exception.
λ> asynchronous -- ThreadKilled wasn't caught
This relies on a convention, not on something the compiler can check or
enforce. SomeAsyncException
can be thrown synchronously, ErrorCall
could
be thrown asynchronously—nothing prevents that. If, however, we trust that
every exception to be thrown asynchronously has a correctly defined
Exception
instance that wraps it with SomeAsyncException
, we are safe.
An alternative solution is the following: we run the action to catch
exceptions from in a separate thread forked with e.g. withAsync
(we will
refer to it as worker thread) and setup an exception handler that catches
all exceptions in that thread. When a synchronous exception is thrown there
we catch it, pack result in Either SomeException a
and return to the main
thread, where we can do whatever we like with it. If an asynchronous
exception strikes in the main thread, it propagates to the worker thread and
shuts it down.
safe-exceptions
It would be an omission not to mention the
safe-exceptions
package which solves the problem of
asynchronous vs synchronous exceptions in a systematic way. The library
defines functions like catch
which only catch synchronous exceptions by
testing the type of exception using the SomeAsyncException
wrapper.
Next, it uses the following logic (taken from the readme of the package):
-
If the user is trying to install a cleanup function (such as with
bracket
orfinally
), we don’t care if the exception is synchronous or asynchronous: call the cleanup function and then re-throw the exception. -
If the user is trying to catch an exception and recover from it, only catch synchronous exceptions and immediately re-throw asynchronous exceptions.
It should be noted that unliftio
also provides exception-handling
functions with the same behavior as the ones in safe-exceptions
. The
difference is that the functions are lifted with MonadUnliftIO
instead of
being defined in terms of classes from exceptions
. We recommend just using
the UnliftIO.Exception
module from unliftio
.