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 useExceptTto 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
handleResultis 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
safeIOEdit 2:
safeIOin all snippetshandleResultEdit 3: Replace
safeIOwithcatchIOError.