http
does not throw IOException
, it throws HttpException
and InternalIOException
is one of the latter's constructors.
You should either catch HttpException
or SomeException
in case you want to catch all exceptions.
Question
Same question as in here, but within a generic MonadResource
instance rather than an explicit ResourceT m
.
How would you define a catch
function such that:
import Control.Exception (Exception, IOException)
import Control.Monad.Trans.Resource (MonadResource, runResourceT)
catch :: (MonadResource m, Exception e) -> m () -> (e -> m ()) -> m ()
catch = undefined
-- 'a' and 'b' are functions from an external library,
-- so I can't actually change their implementation
a, b :: MonadResource m => m ()
a = -- Something that might throw IO exceptions
b = -- Something that might throw IO exceptions
main :: IO ()
main = runResourceT $ do
a `catch` \(e :: IOException) -> -- Exception handling
b `catch` \(e :: IOException) -> -- Exception handling
The problems I run into are:
Control.Exception
, catch
only works on bare IO
s ;Control.Exception.Lifted
, catch
requires an instance of MonadBaseControl
, which MonadResource
is unfortunately not (and I wonder why) ;MonadResource
implies MonadThrow
which defines a monadThrow
function without its 'catch' equivalent (and I wonder why) ;It looks like the only way to handle IO
exceptions is to exit the ResourceT
layer, and this bothers me: I'd like to be able to handle exceptions locally without travelling through the monad transformers stack.
For information, in my real code, a
and b
are actually the http
function from Network.HTTP.Conduit
.
Thank you for your insights.
Compilable with ghc --make example.hs
with http-conduit
library installed:
{-# LANGUAGE FlexibleContexts, ScopedTypeVariables #-}
import Control.Exception.Lifted (IOException, catch)
import Control.Monad.Base (liftBase)
import Control.Monad.Error (MonadError(..), runErrorT)
import Control.Monad.Trans.Control (MonadBaseControl)
import Control.Monad.Trans.Resource (MonadResource, runResourceT)
import Data.Conduit
import Data.Conduit.List (consume)
import Data.Conduit.Text (decode, utf8)
import Data.Text (Text)
import Network.HTTP.Client
import Network.HTTP.Conduit (http)
main :: IO ()
main = do
result <- runErrorT $ runResourceT f
putStrLn $ "OK: " ++ show result
f :: (MonadBaseControl IO m, MonadResource m, MonadError String m) => m [Text]
f = do
req <- liftBase $ parseUrl "http://uri-that-does-not-exist.abc"
manager <- liftBase $ newManager defaultManagerSettings
response <- (http req manager `catch` \(e :: IOException) -> throwError $ show e)
response $$+- decode utf8 =$ consume
When executed, this program ends in error with the following output:
InternalIOException getAddrInfo: does not exist (Name or service not known)
Solution
http
does not throw IOException
, it throws HttpException
and InternalIOException
is one of the latter's constructors.
You should either catch HttpException
or SomeException
in case you want to catch all exceptions.
OTHER TIPS
The type you need,
a, b :: MonadResource m, MonadBaseControl IO m => m ()
Is a special case of the type you currently have
a, b :: MonadResource m => m ()
as the only difference is the extra class constraint. You are free to make the type signatures in your code less general than they would be by default; therefore, changing the signatures of a
and b
should be enough.
If I understand your problem correctly, there is no problem with using lifted-base. Although the type of a
and b
only use the constraint MonadResource m
, it doesn't mean you can't use them on a monad that has other additional properties. For example, if you perform your computation inside ResourceT
, it satisfies the constraint for a
and b
, and you can also use anything from Control.Exception.Lifted
:
-- ...
import Control.Exception.Lifted
-- 'a' and 'b' are functions from an external library,
-- so I can't actually change their implementation
a, b :: MonadResource m => m ()
a = undefined -- Something that might throw IO exceptions
b = undefined -- Something that might throw IO exceptions
main :: IO ()
main = runResourceT $ do
a `catch` \(e :: IOException) -> undefined -- Exception handling
b `catch` \(e :: IOException) -> undefined -- Exception handling
If you alter the type signature of catch
to include MonadCatch
from the exceptions package, then it would be trivial:
import Control.Monad.Trans.Resource (MonadResource, runResourceT)
import Control.Monad.Catch (catch)
a, b :: MonadResource m => m ()
a = …
b = …
main :: IO ()
main = runResourceT $ do
a `catch` \e -> …
b `catch` \e -> …
Note that this does not require any changes for a
nor b
.
Also, both duplode and Petr Pudlák have pointed out that you are free to make the monad for catch
to be as specific as you like, because doing so does not require any cooperation from a
or b
. So any of these solutions will work.