What is the benefit of the MonadCatch?

There are quite a few ways to fail, but in the end all failures are translated into three categories:

  • Pure failure handling, something like a Maybe or Either monads. This is the best way to handle failure, but it is not always possible
  • Exceptions. These can be thrown from pure code, but you really should always avoid doing that. Therefore if you need to handle exceptions, stay in something like an IO monad.
  • Asynchronous exceptions. These can pop at any time. They should never be recovered from and in general should be used in extremely rare case.

Here are some of the ways to fail, that should really be avoided:

  • undefined - when evaluated is translated into runtime exception. The worst way to fail and is only justified as an argument to some existing functions where it won't be evaluated, eg. sizeOf, alignment, etc. These sort of functions should be written with Proxy instead, but that is orthogonal.
  • error - also translates into runtime exception. Should be used only in impossible cases that can never happen.
  • throw - same as error, but allows throwing specific exceptions. Should also be avoided, cause due to laziness it might get evaluated in places where you least expect it.
  • fail - for most monads implementation is to throw an error (default implementation). As pointed out by @chepner, it was designed for pattern match failure and shouldn't really be used. Nevertheless it is still popular, especially in parsing.

All of the above should be avoided, since their usage results in runtime exceptions from pure code.

Proper way to fail:

  • Maybe, Either, Validation, etc. fail purely without exceptions.
  • throwIO - proper way to throw exceptions, when in MonadIO
  • throwSTM - correct way to throw exceptions if you are in STM.
  • throwM - has a appropriate failure implementation that depends on a concrete Monad. In other words it defers the decision on how to fail to the user of the function, which can be pure or not, depending on the monad.

With the preface over let's get to the actual question.

Here is a good example of why fail is bad:

λ> let unsafeDiv x y = if y == 0 then fail "Division by zero" else pure (x `div` y)
λ> 5 `unsafeDiv` 0 :: Maybe Int
Nothing
λ> 5 `unsafeDiv` 0 :: Either String Int
*** Exception: Division by zero
λ> 5 `unsafeDiv` 0 :: IO Int
*** Exception: user error (Division by zero)

STM is another example where fail is really bad, since it results in a call to default implementation: errorWithoutStackTrace :: [Char] -> a. (see throwSTM on why it's bad)

So with fail we will get not only different exceptions, but also incorrect behavior.

On the other hand we have MonadThrow:

λ> let safeDiv x y = if y == 0 then throwM DivideByZero else pure (x `div` y)
λ> 5 `safeDiv` 0 :: Maybe Int
Nothing
λ> 5 `safeDiv` 0 :: Either SomeException Int
Left divide by zero
λ> 5 `safeDiv` 0 :: IO Int
*** Exception: divide by zero

We will always get the same exception that was thrown, granted that the monad supports its propagation. As a consequence of that we can always catch the exception that was thrown. It guarantees ordering, so the exception will not escape due to laziness.

The most correct answer to your question, I think, is to use the failure method that is specific to the monad you are in, but if you don't know the exact monad ahead of time, and want to let the user of your function to choose how to fail, go for throwM

On a related topic I would advise against using MonadCatch and instead use something like unliftio or safe-exceptions. See more info about exception handling here.

Tags:

Haskell