Catching an exception using MonadCatchIO
Imagine you’re working with monad transformers and want to catch an exception. But because the signature of catch is Exception e => IO a -> (e -> IO a) -> IO a
, you need to unwrap monad stacks to do it.
For example, you can catch an exception inside MaybeT monad like:
{-# LANGUAGE DeriveDataTypeable, NoMonomorphismRestriction, ScopedTypeVariables #-}
import Control.Exception
import Control.Monad.IO.Class
import Control.Monad.Trans.Maybe
import Data.Typeable
import Prelude hiding (catch)
data TestException = TestException deriving (Show, Typeable)
instance Exception TestException
test = runMaybeT $ catchMaybeT (liftIO $ throwIO TestException)
(\(e :: TestException) -> return 0)
catchMaybeT m f = MaybeT $ runMaybeT m `catch` (\e -> runMaybeT $ f e)
MonadCatchIO class offers this kind of handlers for most monad transformers. You can use Control.Monad.CatchIO.catch instead of catchMaybeT
above. It also provides bracket families.
But care must be taken when you use bracket with some monad transformers, because the final computation may not run.
For example, in this code, “End” will never be printed.
test = runMaybeT $ CatchIO.bracket (liftIO $ print "Start")
(const $ liftIO $ print "End")
(const empty)
This is because the final computation runs just after the main computation if no exception is thrown, and once a computation becomes Nothing, the computations following that computation will become Nothing in MaybeT monad. This is just the same as
runMaybeT $ empty >> liftIO (print "x")
never printing “x”.
The same goes for ErrorT monad.
Another point I’d like to mention is about a state when each computation runs in StateT.
{-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables #-}
import Control.Exception
import Control.Monad.CatchIO as CatchIO
import Control.Monad.IO.Class
import Control.Monad.Trans.State
import Data.Typeable
data TestException = TestException deriving (Show, Typeable)
instance Exception TestException
test f = runStateT go "Init"
where
go = do CatchIO.bracket (updateState "Start")
(const $ updateState "End")
(const f)
`CatchIO.catch` \(_ :: TestException) -> updateState "Catch"
updateState "Last"
updateState at = do printState at
put at
return at
printState at = get >>= \state -> liftIO $ print $ "At " ++ at ++ ": " ++ state
test1 :: IO (String, String)
test1 = test $ do updateState "Func"
liftIO $ throwIO TestException
In this code, you’ll find that the final computation runs with the state at the start, and the catch computation runs with the initial state, if an exception is thrown. I mean any changes made to the state in the main computation are discarded, although any changes in the base IO monad won’t be reverted — actually it’s impossible.