Reputation: 4060
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
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
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
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