Search code examples
haskellexceptionscotty

Why a pattern-matching failure is not catching by the exception handler?


Why pattern-matching failure is not catching by my exception handler excToStr in this case?

I have a handler of an incoming POST request as under the control of the Scotty Web framework:

...
import qualified Web.Scotty as W
...

    W.post "/some/endpoint" $ excToStr "Cannot handle it!" $ do
        b <- W.body

        -- throwString "aaa" <--- THIS IS HANDLED FINE!

        Just x::Maybe SomeMyType <- pure (decode b) -- BUT NOT THIS PATTERN-MATCHING FAILURE!
        liftIO $ print $ show x
        W.text "OK"

where excToStr is mine and it looks like:

...
import qualified Data.Text.Lazy as LT
...

excH :: (String -> String) -> ActionT LT.Text IO () -> ActionT LT.Text IO ()
excH mkErr m = catchAnyDeep m (W.text . cs . mkErr . show)

excToStr :: String -> ActionT LT.Text IO () -> ActionT LT.Text IO ()
excToStr errMsg = excH (\details -> errMsg <> " (" <> details <> ")")

catchAnyDeep is from safe-exceptions library. I tried another functions as well (catchAny, handle, catch, etc) - without success. The crux of the problem is that when incoming body cannot be decoded successfully (and decode returns Nothing instead of Just x), then pattern-match fails, so I expect that my extToStr (ie. excH) will handle it because catchAnyDeep (and catchAny) handles ANY exception (including pattern-match failures, right?):

catchAny :: MonadCatch m => m a -> (SomeException -> m a) -> m a

and

catchAnyDeep :: (MonadCatch m, MonadIO m, NFData a) => m a -> (SomeException -> m a) -> m a.

If I throw just an exception with throwString then it works as expected (the exception gets caught). But the pattern matching failure leads to an HTTP internal error 500 with a message "Pattern match failure in do expression at....". How to handle pattern-matching exceptions as well?


Solution

  • There are two forms of exceptions in a scotty action (of type ActionT Text IO). There are native exceptions in IO, and another form added by the ActionT transformer. These exceptions are handled separately. The interface is given by the instances of ActionT:

    • (MonadCatch m, ScottyError e) => MonadCatch (ActionT e m) (and similarly the MonadThrow instance). This shows that when you use catch from MonadCatch (or throwString from MonadThrow, and other variants from the safe-exceptions library), you are using the error handling mechanism of the transformed monad m, which in Scotty is usually IO (it defines ActionM = ActionT Text IO).

    • (Monad m, ScottyError e) => MonadFail (ActionT e m). MonadFail is the constraint used for partial pattern-matches in do blocks. It does not require a MonadFail from the underlying monad m, which indicates that, unlike MonadThrow/MonadCatch, it uses the exception mechanism provided by the ActionT transformer itself. To catch this exception you must look for a combinator in Scotty rather than an auxiliary library, such as rescue.