lambdas
lambdas

Reputation: 4060

Transparent error handling

I'm developing REST backend using Scotty and Persistent and I can't figure out the right way to handle errors.

I have several functions to access DB like:

getItem :: Text -> SqlPersistM (Either Error Item)

It returns either inside sql monad. Then I'm using it in my action to retrieve item and return its JSON representation:

get "/items/:name" $ do
    name       <- param "name"
    eitherItem <- lift $ MyDB.getItem name

    case eitherItem of

      Left NotFound -> do
        status status404
        json NotFound

      Left InvalidArgument -> do
        status status400
        json BadRequest

      Right item -> json item

I could make code prettier by introducing some helpers, but the pattern will remain the same - access db, check for error, render appropriate response.

I would like to get rid of error handling in my actions completely:

get "/items/:name" $ do
    name <- param "name"
    item <- lift $ MyDB.getItem name

    -- In case of error, appropriate
    -- HTTP response will be sent,
    -- else just continue

    bars <- lift $ MyDB.listBars

    -- In case of error, appropriate
    -- HTTP response will be sent,
    -- else just continue

    json (process item bars)

I.e. getItem may return error, and it will be transformed to json response somehow, all transparently to the action code. It would be nice if getItem will know nothing about actions and json responses.

I've solved this problem in the past using imperative languages by throwing exceptions from everywhere, then catching it in one place and render appropriate responses. I guess it is possible with Haskell too, but I want to know how to solve this problem by using functional tools.

I know it is possible for monads to short-circuit (like Either >> Either >> Either) but have no idea how to use it in this slightly more complicated case.

Upvotes: 2

Views: 233

Answers (3)

Cirdec
Cirdec

Reputation: 24156

You are looking for the Error monad.

You want to write something like:

get "/items/:name" $ handleErrorsInJson do
    name <- param "name"
    item <- lift $ MyDB.getItem name    
    bars <- lift $ MyDB.listBars    
    json (process item bars)

transformers' ErrorT adds error handling to an existing Monad.

To do so, you need to make your data access methods indicate that they encountered an error in the Error monad instead of by returning Either

getItem :: Text -> SqlPersistM (Either Error Item)

Alternatively you could use something like

toErrors :: m (Either e a) -> ErrorT e m a

to use your existing functions without modifying them. A quick Hoogling indicates there's already something with the type m (Either e a) -> ErrorT e m a, and that is the constructor ErrorT. Equipped with this we could write:

get "/items/:name" $ handleErrorsInJson do
    name <- lift $ param "name"
    item <- ErrorT $ lift $ MyDB.getItem name    
    bars <- ErrorT $ lift $ MyDB.listBars    
    lift $ json (process item bars)

What would handleErrorsInJson be? Borrowing handleError from Ankur's example:

handleErrorsInJson :: ErrorT Error ActionM () -> ActionM ()
handleErrorsInJson = onError handleError

onError :: (e -> m a) -> (ErrorT e m a) -> m a
onError handler errorWrapped = do
    errorOrItem <- runErrorT errorWrapped
    either handler return errorOrItem 

Note: I didn't check this against a compiler, there could be small mistakes in here. Editted to fix problems after seeing Gabriel's response. handleErrorsInJson wouldn't type check, it was missing a much-needed runErrorT.

Upvotes: 2

Gabriella Gonzalez
Gabriella Gonzalez

Reputation: 35089

The solution is to use the EitherT monad transformer (from the either package) to handle short-circuiting of errors. EitherT extends any monad with functionality exactly like checked exceptions from an imperative language.

This works for any "base" monad, m, and let's assume that you have two types of computations, some of which fail and some of which never fail:

fails    :: m (Either Error r)  -- A computation that fails
succeeds :: m r                 -- A computation that never fails

You can then lift both of these computations to the EitherT Error m monad. The way you lift failing computations is to wrap them in the EitherT constructor (the constructor has the same name as the type):

EitherT :: m (Either Error r) -> EitherT Error m r

EitherT fails :: EitherT Error m r

Notice how the Error type is now absorbed into the monad and doesn't show up in the return value any longer.

To lift a successful computation, use lift, from transformers:

lift :: m r -> EitherT Error m r

lift succeeds :: EitherT Error m r

The type of lift is actually more general, because it works for any monad transformer. Its general type is:

lift :: (MonadTrans t) => m r -> t m r

... where in our case t is EitherT Error.

Using both of these tricks you can then convert your code to automatically short-circuit on errors:

import Control.Monad.Trans.Either

get "/items/:name" $ do
    eitherItem <- runEitherT $ do
        name <- lift    $ param "name"
        item <- EitherT $ lift $ MyDB.getItem name
        bars <- EitherT $ lift $ MyDB.listBars
        lift $ json (process item bars)
    case eitherItem of
        Left NotFound -> do
            status status404
            json NotFound
        Left InvalidArgument -> do
            status status400
            json BadRequest
        Right () -> return ()

runEitherT runs your EitherT up until it completes or it encounters the first error. The eitherItem that runEitherT returns will be a Left if the computation failed or a Right if the computation succeeded.

This lets you condense your error handling into a single case statement after the block.

You can even do catch-like behavior if you import catch from Control.Error which is provided by my errors package. This lets you write code very similar to imperative code:

(do
    someEitherTComputation
    more stuff
) `catch` (\eitherItem -> do
    handlerLogic
    more stuff
)

However, you will still need to use runEitherT at some point in your code to unwrap the EitherT when you are done, even if you caught and handled the error. That's why for this simpler example I recommend just using runEitherT directly rather than catch.

Upvotes: 4

Ankur
Ankur

Reputation: 33637

What you need is a function like below, which can map a Error to an ActionM:

handleError :: Error -> ActionM ()
handleError NotFound = status status404 >> json NotFound
handleError InvalidArgument = status status400 >> json BadRequest
...other error cases...

respond :: ToJSON a => Either Error a -> ActionM ()
respond (Left e) = handleError e
respond (Right item) = json item

Then in your handler function use the above function as:

get "/items/:name" $ do
    name       <- param "name"
    eitherItem <- lift $ MyDB.getItem name
    respond eitherItem

Upvotes: 1

Related Questions