Catching What We’ve Thrown

Last week we learned how to throw exceptions in Haskell. In the next couple articles, we're going to learn how to "catch" them, so that in exceptional circumstances we can still proceed with our program in a sane way.

Now, throwing exceptions disrupted our patterns of type safety quite a bit. We could throw an exception from any piece of seemingly pure code. Even our simple function from a list to an element of that list could invoke throw:

data ListException = ListIsEmpty | NotEnoughElements
  deriving (Show)

instance Exception ListException

myHead :: [a] -> a
myHead [] = throw ListIsEmpty
myHead (a : _) = a

sum2Pairs :: (Num a) => [a] -> (a, a)
sum2Pairs (a : b : c : d : _) = (a + b, c + d)
sum2Pairs _ = throw NotEnoughElements

Unlike throwing exceptions though, we can only "catch" exceptions in the IO monad. As we discussed last month, the IO monad involves a lot of operations to communicate with the outside world, and so it is the most "impure" of monads. Part of this impurity is that we can "intercept" exception siganls that are sent to the operating system.

The first function we'll go over this time for catching exceptions is, well, catch. Here's its type signature:

catch :: (Exception e)
  => IO a
  -> (e -> IO a)
  -> IO a

It takes an IO action we would like to perform and then a "handler" for a particular kind of exception that can occur. The handler takes the exception as an input and then produces a new IO action with the same return value. Here's how we can use it in our example:

main :: IO ()
main = do
  catch (return (sum2Pairs [2, 3, 4]) >>= print) handler
  catch (return (sum2Pairs [2, 3, 4, 5]) >>= print) handler
  where
    handler :: ListException -> IO ()
    handler e = print e

...

>> stack exec my-program
NotEnoughElements
(5, 9)

Notice we need to wrap our pure computation sum2Pairs in the IO monad using return to catch its exception. Then we need to make it so our handler function returns the same type. In this case, we make that type () and just print the results.

Two final notes. First, the function handle is the same as catch except its arguments are reversed.

handle :: (Exception e)
  => IO a
  -> (e -> IO a)
  -> IO a

This can make for cleaner code in our example. We can put our handler function first and use do-syntax for the computation itself. This is good with lengthier examples.

main :: IO ()
main = do
  handle handler $ do
    result <- return (sum2Pairs [2, 3, 4])
    print result
  handle handler $ do
    result <- return (sum2Pairs [2, 3, 4, 5])
    print result
  where
    handler :: ListException -> IO ()
    handler e = print e

Second, our handler will only catch exceptions that match the type of the handler! We specified the handler as a separate expression with its own type signature because you need to specify what the type is! It wouldn't work to just inline this definition, because GHC would complain about an ambiguous type. So for example, if we opened a non-existant file, our handler would not catch this, and the program would crash:

main :: IO ()
main = do
  handle handler $ readFile "does_not_exist.txt" >>= print
  handle handler $ do
    result <- return (sum2Pairs [2, 3, 4, 5])
    print result
  where
    handler :: ListException -> IO ()
    handler e = print e

...

>> stack exec my-program
my-program: does_not_exit.txt: openFile: does not exist (No such file or directory)

It is possible to catch all exceptions, but this is not advisable, as the documentation says. We'll go into more details about that possibility later.

For now, you should check out one of our useful resources for whatever stage of your Haskell journey you are at! If you're just starting out, our Beginners Checklist will help you out. If you're looking to incorporate exceptions into a larger project, try out our production checklist for some more suggestions of libraries to use!

Previous
Previous

"Try"-ing It Out First

Next
Next

Throwing Exceptions: The Basics