Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Catching an Exception from runDb

This is a follow-up to my previous post. MaybeT and Transactions in runDb

I thought this will be a simple thing to do but I have been trying to figure this out for over a day and still haven't made much progress. So thought I will give up and ask!

I just added a try function (from Control.Exception.Lifted) to my previous code and I couldn't get the code to type check. Variants like catch and handle had similar issues.

eauth <- LiftIO (
           try( runDb $ do
                ma <- runMaybeT $ do
                   valid <- ... 
                case ma of
                  Just a -> return a
                  Nothing -> liftIO $ throwIO MyException
            )  :: IO (Either MyException Auth) 
          )
case eauth of
    Right auth -> return auth
    Left _     -> lift $ left err400 { errBody = "Could not create user"}

My runDb looks like this (I also tried a variant where I removed liftIO):

runDb query = do
    pool <- asks getPool
    liftIO $ runSqlPool query pool

I get this error:

No instance for (Control.Monad.Reader.Class.MonadReader Config IO)
  arising from a use of ‘runDb’
In the expression: runDb
In the first argument of ‘try’, namely
  ‘(runDb
    $ do { ma <- runMaybeT ... 

I am running inside servant handler and my return type is AppM Auth where

 type AppM = ReaderT Config (EitherT ServantErr IO)

I have tried many combinations of lifting but doesn't seem to be helping. I thought I will take this opportunity to figure out things from scratch and I hit a wall as well. If someone could suggest how you arrived at the answer, it will be super instructive for me.

This has been my thought process:

  1. I see runSqlConn :: MonadBaseControl IO m => SqlPersistT m a -> Connection -> m a
  2. So that seems to imply it will be in the IO monad, which means try should work
  3. I think check the definition of MonadBaseControl which has class MonadBase b m => MonadBaseControl b m | m -> b. At this point I am confused. This functional dependency logic seems to be suggest type m dictates what b will be but in the previous one b was specified as IO.
  4. I check MonadBase and that did not give me any clue either.
  5. I check SqlPersistT and got no clues either.
  6. I reduced the problem to something very simple like result <- liftIO (try (evaluate (5 `div` 0)) :: IO (Either SomeException Int)) and that worked. So I was even more confused at this time. Doesn't runDb work in IO so shouldn't the same thing work for my original code?

I thought I can figure this out by backtracking but it seems like my level of Haskell knowledge is just not sufficient to get at the root of the problem. Appreciate if people can provide step by step pointers as to arrive at the right solution.

Thanks!

like image 863
Ecognium Avatar asked Dec 30 '25 22:12

Ecognium


1 Answers

General type signature for try:

(MonadBaseControl IO m, Exception e) => m a -> m (Either e a)

Specialized type signature for try (as it appears in your code):

IO Auth -> IO (Either MyException Auth)

So, the monadic value that is the argument to try has type:

IO Auth

Everything listed above, you probably already understood. If we look at the type signature for your runDb, we get this:

runDb :: (MonadReader Config m, MonadIO m) => SqlPersistT m a -> m a

I sort of had to guess because you didn't provide a type signature, but that is probably what it is. So now, the problem should be a little clearer. You are trying to use runDb to create a monadic value for something that's supposed to be in IO. But IO doesn't satisfy the MonadReader Config instance that you need.

To make the mistake more clear, let's make runDb more monomorphic. You could give it this type signature instead:

type AppM = ReaderT Config (EitherT ServantErr IO)
runDb :: SqlPersistT AppM a -> AppM a

And now if you tried to compile your code, you would get an even better error. Instead of telling you

No instance for (Control.Monad.Reader.Class.MonadReader Config IO)

It would tell you that IO doesn't match AppM (although it would probably expand the type synonym). Practically, what this means is that you can't get the shared pool of database connections magically out of IO. You need the ReaderT Config that was passing it around everywhere.

The easiest fix I can think of would be to stop using exceptions where they aren't necessary:

mauth <- runDb $ runMaybeT $ do
            ... -- Same stuff you were doing earlier 
case mauth of
    Just auth -> return auth
    Nothing   -> lift $ left err400 { errBody = "Could not create user"}
like image 116
Andrew Thaddeus Martin Avatar answered Jan 01 '26 13:01

Andrew Thaddeus Martin



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!