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

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
- Philipp, Hagenlocher. 2020. Haskell for Imperative Programmers #27 - Exceptions. YouTube.