Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Catch IO exceptions within an instance of MonadResource

Short version

Same question as in here, but within a generic MonadResource instance rather than an explicit ResourceT m.

Long version

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:

  • In Control.Exception, catch only works on bare IOs ;
  • In 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.

Minimal code with the problem

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)
like image 706
koral Avatar asked Feb 03 '26 14:02

koral


2 Answers

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.

like image 67
Tobias Brandt Avatar answered Feb 05 '26 08:02

Tobias Brandt


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.

like image 45
duplode Avatar answered Feb 05 '26 08:02

duplode



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!