Road to Haskeller #19 - Exception

Last Edited: 7/20/2024

The blog post introduces an important concept of exception in Haskell.

Haskell & Exception

Exception

When things don't go well, we have to manage that. For example, the div function would not work when divided by 0, so there needs to be a way of handling this. We did this with Maybe before, but we can use Exception as well. Exception is a typeclass with many instances like the below.

import Control.Exception -- Import
 
-- Examples
instance Exception SomeAsyncException
instance Exception IOException
instance Exception ArithException
instance Exception TypeError
--- and more

To throw an exception, you can do the following.

divide = do
    x <- getLine
    y <- getLine
    let  x' =  read x :: Float
         y' = read y :: Float
    if y' == 0 then throw DivideByZero else do return $ x' / y'

The above is throwing a DivideByZero exception in the Control.Exception module using throw when y provided by the user is 0. You can also define your own exception and throw it like below.

data MyError = ErrorA deriving Show
instance Exception MyError
 
throw ErrorA -- => ***Exception: ErrorA

You don't need to define functions for your error to be an instance of Exception, but it needs to belong to the Show typeclass.

Catching

The exception needs to be caught for it to be displayed to the user. As it involves displaying on the screen, the exception can only be caught in impure code or the IO monad. Let's catch the exception from divide in the main IO action.

-- Function for catching an exception in an IO action
catch :: Exception e => IO a -> (e -> IO a) -> IO a
 
main :: IO Float
main = do
    catch divide (\e -> 
      do 
      let t = e :: ArithException
      putStrLn "You divided by 0. "
      return 0
      )

The above catch function takes an IO action with a potential exception, a handler for the exception, and returns an IO action of the same type. Here, the function is used on divide with an ArithException handler as DivideByZero is a value constructor of the ArithException datatype. The handler prints "You divided by 0." to warn user, while returning 0 to ensure it is an IO Float, the same type as divide.

You could have used SomeException for the exception since all the exceptions are SomException, but it is not the best practice as the handler should be defined differently for each exception. When catching mulitple exceptions, we can do the following:

f = <expr> `catch` \ (ex :: ArithException) -> handleArith ex
           `catch` \ (ex :: IOException) -> handleIO ex

The above looks good, right? No. Actually, it is a bad practice to use catch like that, because it might catch an IOException from the handleArith handler. What we want is to catch ArithException or IOException only from <expr>. To accomplish this, we can use the catches function.

-- Catching multiple exceptions
catches :: Exception e => IO a -> [Handler (e -> IO a)] -> IO a
 
-- Rewritten with catches
f = <expr> `catches` [Handler (\ (ex :: ArithException) -> handleArith ex),
                      Handler (\ (ex :: IOException) -> handleIO ex)]

Try

Another way of "catching" an exception is by using the try function. It takes an IO action and outputs an IO action of type Either that contains the exception in Left or the result of computation of in Right.

-- Try function
try :: Exception e => IO a -> IO (Either e a)
 
main :: IO Float
main = do
    result <- try divide :: IO (Either ArithException Float)
    case result of
        Left ex -> do 
          putStrLn "You divided by 0. " 
          return 0
        Right val -> return val

It achieves the same thing as catch except that try can get intrupted by an asynchroneous exception while catch can't. Also, when you want to catch an exception from a pure function, you need to force execution of the pure function by using evaluate.

-- Pure divide function that throws exception
divide x y
  | y == 0 = throw DivideByZero
  | otherwise = x / y
 
-- Using evaluate for pure function execution
main = do
    result <- try (evaluate(divide 5 0)) :: IO (Either ArithException Float)
    case result of
        Left ex -> do 
          putStrLn "You divided by 0. " 
        Right val -> print val

Aside from try, catch, and catches, there are other predefined functions like tryJust, catchJust, and finally. If you are curious about them, google them! (Googling is one of the most important skill for a programmer. )

When to use Exception

We have covered quite a lot about Exception, but how do we choose between Maybe, Either, or Exception? The simplest answer is: use Maybe and Either as much as possible unless it is unavoidable to use Exception because Exception is not really functional. It is unavoidable in cases where you are dealing with IO, threads, and system. Otherwise, just try using Maybe or Either.

Exercises

This is an exercise section where you can test your understanding of the material introduced in the article. I highly recommend solving these questions by yourself after reading the main part of the article. You can click on each question to see its answer.

Resources