I'd like to defer actions. Therefore I use a WriterT
that should remember actions that I tell
him.
module Main where
import Control.Exception.Safe
(Exception, MonadCatch, MonadThrow, SomeException,
SomeException(SomeException), catch, throwM)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Writer (WriterT, runWriterT, tell)
type Defer m a = WriterT (IO ()) m a
-- | Register an action that should be run later.
defer :: (Monad m) => IO () -> Defer m ()
defer = tell
-- | Ensures to run deferred actions even after an error has been thrown.
runDefer :: (MonadIO m, MonadCatch m) => Defer m () -> m ()
runDefer fn = do
((), deferredActions) <- runWriterT (catch fn onError)
liftIO $ do
putStrLn "run deferred actions"
deferredActions
-- | Handle exceptions.
onError :: (MonadIO m) => MyException -> m ()
onError e = liftIO $ putStrLn $ "handle exception: " ++ show e
data MyException =
MyException String
instance Exception MyException
instance Show MyException where
show (MyException message) = "MyException(" ++ message ++ ")"
main :: IO ()
main = do
putStrLn "start"
runDefer $ do
liftIO $ putStrLn "do stuff 1"
defer $ putStrLn "cleanup 1"
liftIO $ putStrLn "do stuff 2"
defer $ putStrLn "cleanup 2"
liftIO $ putStrLn "do stuff 3"
putStrLn "end"
I get the expected output
start
do stuff 1
do stuff 2
do stuff 3
run deferred actions
cleanup 1
cleanup 2
end
However, if an exception is thrown
main :: IO ()
main = do
putStrLn "start"
runDefer $ do
liftIO $ putStrLn "do stuff 1"
defer $ putStrLn "cleanup 1"
liftIO $ putStrLn "do stuff 2"
defer $ putStrLn "cleanup 2"
liftIO $ putStrLn "do stuff 3"
throwM $ MyException "exception after do stuff 3"
putStrLn "end"
none of the deferred actions is run
start
do stuff 1
do stuff 2
do stuff 3
handle exception: MyException(exception after do stuff 3)
run deferred actions
end
but I expect this
start
do stuff 1
do stuff 2
do stuff 3
handle exception: MyException(exception after do stuff 3)
run deferred actions
cleanup 1
cleanup 2
end
The writer somehow looses his state. If I use [IO ()]
as state instead of IO ()
type Defer m a = WriterT [IO ()] m a
and print the length of deferredActions
in runDefer
it is 2 on success (because I called defer
twice) and 0 on error (even though defer
has been called twice).
What causes this issue? How can I run the deferred actions after an error?
Like user2407038 already explained it is not possible to get the state (deferred actions) in
catch
. However, you can useExceptT
to catch errors explicitly:We get the expected output:
Notice that you have to catch errors explicitly using
catchIOError
. If you forget it and just callliftIO
, the error will not be caught.Note further that the call to
handleResult
is not safe. If it throws an error the deferred actions won't be run afterwards. You might consider to handle the result after the actions have been run:Otherwise, you have to catch that error separately.
Edit 1: Introduce
safeIO
Edit 2:
safeIO
in all snippetshandleResult
Edit 3: Replace
safeIO
withcatchIOError
.