There is a way, but let me first explain recovering state from errors in terms of ErrorT
and StateT
, because I find that it illuminates the general case very well.
Let's first imagine the case where ErrorT
is on the outside of StateT
. In other words:
m1 :: ErrorT e (StateT s m) r
If you unwrap both the ErrorT
and StateT
newtypes you get:
runErrorT m1
:: StateT s m (Either e r)
runStateT (runErrorT m1)
:: s -> m (Either e r, s)
The unwrapped type says that we recover the final state, even if we receive an error. So just remember that ErrorT
on the outside of StateT
means we can recover from errors while still preserving the current state.
Now, let's switch the order:
m2 :: StateT s (ErrorT e m r)
runStateT m2
:: s -> ErrorT e m (r, s)
runErrorT . runStateT m2
:: s -> m (Either e (r, s))
This type tells a different story: we only recover the ending state if our computation succeeds. So just remember that ErrorT
on the inside of StateT
means that we can't recover the state.
This might seem curious to somebody familiar with the mtl
, which provides the following MonadError
instance for StateT
:
instance (MonadError e m) => MonadError e (StateT s m) where ...
How does StateT
recover gracefully from errors after what I just said? Well, it turns out that it does not. If you write the following code:
(m :: StateT s (ErrorT e m) r) `catchError` f
... then if m
uses throwError
, f
will begin from m
's initial state, not the state that m
was at when it threw the error.
Okay, so now to answer your specific question. Think of IO
as having a built-in ErrorT
layer by default. This means that if you can't get rid of this ErrorT
layer then it will always be inside your StateT
and when it throws errors you won't be able to recover the current state.
Similarly, you can think of IO
as having a built-in StateT
layer by default that is below the ErrorT
layer. This layer conceptually holds the IORef
s, and because it is "inside" the ErrorT
layer it always survives errors and preserves IORef
values.
This means that the only way you can use a StateT
layer above the IO
monad and have it survive an exception is to get rid of IO
s ErrorT
layer. There is only one way to do this:
Wrap every
IO
action intryIO
Mask asynchronous exceptions and only unmask them in the middle of
tryIO
statements.
My personal recommendation is to go the IORef
route since there are some people who will not be happy about masking asynchronous exceptions outside of tryIO
statements, because then you cannot interrupt pure computations.