Search code examples
haskellservant

servant throwError with custom monad stack


given a monad like this:


newtype App a = App
  { runApp :: ReaderT AppEnv (LoggingT IO) a
  }
  deriving newtype
    ( Functor,
      Applicative,
      Monad,
      MonadIO, 
      MonadReader AppEnv,
      MonadLogger
    )

newtype AppEnv = AppEnv -- environment for the app

I'd like to use the standard error handling from Servant using throwError:

foo :: App ()
foo = throwError err404

which doesn't compile

    • No instance for (Control.Monad.Error.Class.MonadError
                         ServerError App)
        arising from a use of ‘throwError’
    • In the expression: throwError err404
      In an equation for ‘foo’: foo = throwError err404

and I can't find a way to make that work. Can I derive such an instance for App? Do I need to change the monad stack?

I could use throw, but that does change the behavior of servant-client, which I want to avoid.


Solution

  • If we want to throwError values of type ServerError, our monad needs to be an instance of MonadError ServerError. Looking at the available instances, we can get an idea of what we would need to add to our monad stack.

    These instances won't work because they are for specific error types different from ServerError:

    MonadError IOException IO
    MonadError () Maybe
    

    This instance would force us to use Either as our base monad:

    MonadError e (Either e)
    

    Then there are a whole bunch of "passthrough" instances that propagate the MonadError constraint from the base monad, but don't introduce it:

    MonadError e m => MonadError e (IdentityT m)
    MonadError e m => MonadError e (ReaderT r m)
    MonadError e m => MonadError e (StateT s m)
    ...
    

    This is the instance we need:

    Monad m => MonadError e (ExceptT e m)
    

    We need to add an ExceptT ServerError transformer somewhere in our monad stack, and newtype derive MonadError ServerError.

    An annoyance with this solution is that introducing ExceptT might make operations like catch (for runtime exceptions) more difficult to perform.