Error handling is one of the most important—and overlooked—topics for programmers, regardless of the language used. In Haskell, you will find two major types of error handling employed: “pure” error handling and exceptions.
When we speak of “pure” error handling, we are referring to
algorithms that do not require anything from the IO
monad. We
can often implement error handling for them by simply using
Haskell’s expressive data type system to our advantage. Haskell
also has an exception system. Due to the complexities of lazy
evaluation, exceptions in Haskell can be thrown anywhere, but only
caught within the IO
monad. In this chapter, we’ll consider
both.
Let’s begin our discussion of error handling with a very simple function. Let’s say that we wish to perform division on a series of numbers. We have a constant numerator, but wish to vary the denominator. We might come up with a function like this:
divBy :: Integral a => a -> [a] -> [a] divBy numerator = map (numerator `div`)
Very simple, right? We can play around with this a bit in ghci
:
ghci> divBy 50 [1,2,5,8,10]
[50,25,10,6,5]
ghci> take 5 (divBy 100 [1..])
[100,50,33,25,20]
This behaves as expected: 50 / 1
is 50
, 50 / 2
is 25
, and
so forth.[fn:1] This even worked with the infinite list [1..]
.
What happens if we sneak a 0
into our list somewhere?
ghci> divBy 50 [1,2,0,8,10]
[50,25,*** Exception: divide by zero
Isn’t that interesting? ghci
started displaying the output, then
stopped with an exception when it got to the zero. That’s lazy
evaluation at work—it calculated results as needed.
As we will see later in this chapter, in the absence of an explicit exception handler, this exception will crash the program. That’s obviously not desirable, so let’s consider better ways we could indicate an error in this pure function.
One immediately-recognizable easy way to indicate failure is to
use Maybe
.[fn:2] Instead of just returning a list and throwing
an exception on failure, we can return Nothing
if the input list
contained a zero anywhere, or Just
with the results otherwise.
Here’s an implementation of such an algorithm:
divBy :: Integral a => a -> [a] -> Maybe [a] divBy _ [] = Just [] divBy _ (0:_) = Nothing divBy numerator (denom:xs) = case divBy numerator xs of Nothing -> Nothing Just results -> Just ((numerator `div` denom) : results)
If you try it out in ghci
, you’ll see that it works:
ghci> divBy 50 [1,2,5,8,10]
Just [50,25,10,6,5]
ghci> divBy 50 [1,2,0,8,10]
Nothing
The function that calls divBy
can now use a case
statement to
see if the call was successful, just as divBy
does when it calls
itself.
We will be avoiding the monadic implementation in this chapter for simplicity, but wanted to point out that it exists.
The use of Maybe
was convenient, but has come at a cost. divBy
can no longer handle infinite lists as input. Since the result is
Maybe [a]
, the entire input list must be examined before we can
be sure that we won’t be returning Nothing
due to a zero
somewhere in it. You can verify this is the case by attempting one
of our earlier examples:
ghci> divBy 100 [1..]
*** Exception: stack overflow
Note that you don’t start seeing partial output here; you get no
output. Notice that at each step in divBy
(except for the case
of an empty input list or a zero at the start of the list), the
results from every subsequent element must be known before the
results from the current element can be known. Thus this algorithm
can’t work on infinite lists, and it is also not very
space-efficient for large finite lists.
Having said all that, Maybe
is often a fine choice. In this
particular case, we don’t know whether there will be a problem
until we get into evaluating the entire input. Sometimes we know
of a problem up front, for instance, that tail []
in ghci
produces an exception. We could easily write an infinite-capable
tail
that doesn’t have this problem:
safeTail :: [a] -> Maybe [a] safeTail [] = Nothing safeTail (_:xs) = Just xs
This simply returns Nothing
if given an empty input list, or
Just
with the result for anything else. Since we only have to
make sure the list is non-empty before knowing whether or not we
have an error, using Maybe
here doesn’t reduce our laziness. We
can test this out in ghci
and see how it compares with regular
tail
:
ghci> tail [1,2,3,4,5]
[2,3,4,5]
ghci> safeTail [1,2,3,4,5]
Just [2,3,4,5]
ghci> tail []
*** Exception: Prelude.tail: empty list
ghci> safeTail []
Nothing
Here, we can see our safeTail
performed as expected. But what
about infinite lists? We don’t want to print out an infinite
number of results, so we can test with take 5 (tail [1..])
and a
similar construction with safeTail
:
ghci> take 5 (tail [1..])
[2,3,4,5,6]
ghci> case safeTail [1..] of {Nothing -> Nothing; Just x -> Just (take 5 x)}
Just [2,3,4,5,6]
ghci> take 5 (tail [])
*** Exception: Prelude.tail: empty list
ghci> case safeTail [] of {Nothing -> Nothing; Just x -> Just (take 5 x)}
Nothing
Here you can see that both tail
and safeTail
handled infinite
lists just fine. Note that we were able to deal better with an
empty input list; instead of throwing an exception, we decided to
return Nothing
in that situation. We were able to achieve error
handling at no expense to laziness.
But how do we apply this to our divBy
example? Let’s consider
the situation there: failure is a property of an individual bad
input, not of the input list itself. How about making failure a
property of an individual output element, rather than the output
list itself? That is, instead of a function of type
a -> [a] -> Maybe [a]
, instead we will have
a -> [a] -> [Maybe a]
. This will have the benefit of preserving
laziness, plus the caller will be able to determine exactly where
in the list the problem was—or even just filter out the problem
results if desired. Here’s an implementation:
divBy :: Integral a => a -> [a] -> [Maybe a] divBy numerator denominators = map worker denominators where worker 0 = Nothing worker x = Just (numerator `div` x)
Take a look at this function. We’re back to using map
, which is
a good thing for both laziness and simplicity. We can try it out
in ghci
and see that it works for finite and infinite lists just
fine:
ghci> divBy 50 [1,2,5,8,10]
[Just 50,Just 25,Just 10,Just 6,Just 5]
ghci> divBy 50 [1,2,0,8,10]
[Just 50,Just 25,Nothing,Just 6,Just 5]
ghci> take 5 (divBy 100 [1..])
[Just 100,Just 50,Just 33,Just 25,Just 20]
We hope that you can take from this discussion the point that
there is a distinction between the input not being well-formed (as
in the case of safeTail
) and the input potentially containing
some bad data, as in the case of divBy
. These two cases can
often justify different handling of the results.
Back in the section called “Use of Maybe”
program named divby2.hs
. This example didn’t preserve laziness,
but returned a value of type Maybe [a]
. The exact same algorithm
could be expressed using a monadic style. For more information and
important background on monads, please refer to Chapter 14, /Monads/.
Here’s our new monadic-style algorithm:
divBy :: Integral a => a -> [a] -> Maybe [a] divBy _ [] = return [] divBy _ (0:_) = fail "division by zero in divBy" divBy numerator (denom:xs) = do next <- divBy numerator xs return ((numerator `div` denom) : next)
The Maybe
monad has made the expression of this algorithm look
nicer. For the Maybe
monad, return
is the same as Just
, and
fail _ = Nothing
, so our error explanation string is never
actually seen anywhere. We can test this algorithm with the same
tests we used against divby2.hs
if we want:
ghci> divBy 50 [1,2,5,8,10]
Just [50,25,10,6,5]
ghci> divBy 50 [1,2,0,8,10]
Nothing
ghci> divBy 100 [1..]
*** Exception: stack overflow
The code we wrote actually isn’t specific to the Maybe
monad. By
simply changing the type, we can make it work for any monad.
Let’s try it:
divBy :: Integral a => a -> [a] -> Maybe [a] divBy = divByGeneric divByGeneric :: (Monad m, Integral a) => a -> [a] -> m [a] divByGeneric _ [] = return [] divByGeneric _ (0:_) = fail "division by zero in divByGeneric" divByGeneric numerator (denom:xs) = do next <- divByGeneric numerator xs return ((numerator `div` denom) : next)
The function divByGeneric
contains the same code as divBy
did
before; we just gave it a more general type. This is, in fact, the
type that ghci
infers if no type would be given. We also defined
a convenience function divBy
with a more specific type.
Let’s try this out in ghci
.
ghci> :l divby5.hs
[1 of 1] Compiling Main ( divby5.hs, interpreted )
Ok, modules loaded: Main.
ghci> divBy 50 [1,2,5,8,10]
Just [50,25,10,6,5]
ghci> (divByGeneric 50 [1,2,5,8,10])::(Integral a => Maybe [a])
Just [50,25,10,6,5]
ghci> divByGeneric 50 [1,2,5,8,10]
[50,25,10,6,5]
ghci> divByGeneric 50 [1,2,0,8,10]
*** Exception: user error (division by zero in divByGeneric)
The first two examples both produce the same output we see before.
Since divByGeneric
doesn’t have a specific return type, we must
either give one or let the interpreter infer one from the
environment. If we don’t give a specific return type, ghci
infers the IO
monad. You can see that in the third and fourth
examples. The IO
monad converts fail
into an exception, as you
can see with the fourth example.
The Control.Monad.Error
module in the mtl
package makes
Either String
into a monad as well. If you use Either
, you can
get a pure result that preserves the error message, like so:
ghci> :m +Control.Monad.Error
ghci> (divByGeneric 50 [1,2,5,8,10])::(Integral a => Either String [a])
Loading package mtl-1.1.0.0 ... linking ... done.
Right [50,25,10,6,5]
ghci> (divByGeneric 50 [1,2,0,8,10])::(Integral a => Either String [a])
Left "division by zero in divByGeneric"
This leads us into our next topic of discussion: using Either
for returning error information.
The Either
type is similar to the Maybe
type, with one key
difference: it can carry attached data both for an error and a
success (“the Right
answer”).[fn:3] Although the language
imposes no restrictions, by convention, a function returning an
Either
uses a Left
return value to indicate an error, and
Right
to indicate success. If it helps you remember, you can
think of getting the Right
answer. We can start with our
divby2.hs
example from the earlier section on Maybe
and adapt
it to work with Either
:
divBy :: Integral a => a -> [a] -> Either String [a] divBy _ [] = Right [] divBy _ (0:_) = Left "divBy: division by 0" divBy numerator (denom:xs) = case divBy numerator xs of Left x -> Left x Right results -> Right ((numerator `div` denom) : results)
This code is almost identical to the Maybe
code; we’ve
substituted Right
for every Just
. Left
compares to
Nothing
, but now it can carry a message. Let’s check it out in
ghci
:
ghci> divBy 50 [1,2,5,8,10]
Right [50,25,10,6,5]
ghci> divBy 50 [1,2,0,8,10]
Left "divBy: division by 0"
While a String
indicating the cause of an error may be useful to
humans down the road, it’s often helpful to define a custom error
type that we can use to programmatically decide on a course of
action based upon exactly what the problem was. For instance,
let’s say that for some reason, besides 0, we also don’t want to
divide by 10 or 20. We could define a custom error type like so:
data DivByError a = DivBy0 | ForbiddenDenominator a deriving (Eq, Read, Show) divBy :: Integral a => a -> [a] -> Either (DivByError a) [a] divBy _ [] = Right [] divBy _ (0:_) = Left DivBy0 divBy _ (10:_) = Left (ForbiddenDenominator 10) divBy _ (20:_) = Left (ForbiddenDenominator 20) divBy numerator (denom:xs) = case divBy numerator xs of Left x -> Left x Right results -> Right ((numerator `div` denom) : results)
Now, in the event of an error, the Left
data could be inspected
to find the exact cause. Or, it could simply be printed out with
show
, which will generate a reasonable idea of the problem as
well. Here’s this function in action:
ghci> divBy 50 [1,2,5,8]
Right [50,25,10,6]
ghci> divBy 50 [1,2,5,8,10]
Left (ForbiddenDenominator 10)
ghci> divBy 50 [1,2,0,8,10]
Left DivBy0
Back in the section called “Usage of the Maybe Monad”
how to use Maybe
in a monad. Either
can be used in a monad
too, but can be slightly more complicated. The reason is that
fail
is hard-coded to accept only a String
as the failure
code, so we have to have a way to map such a string into whatever
type we used for Left
. As you saw earlier, Control.Monad.Error
provides built-in support for Either String a
, which involves no
mapping for the argument to fail
. Here’s how we can set up our
example to work with Either
in the monadic style:
{-# LANGUAGE FlexibleContexts #-} import Control.Monad.Error data Show a => DivByError a = DivBy0 | ForbiddenDenominator a | OtherDivByError String deriving (Eq, Read, Show) instance Error (DivByError a) where strMsg x = OtherDivByError x divBy :: Integral a => a -> [a] -> Either (DivByError a) [a] divBy = divByGeneric divByGeneric :: (Integral a, MonadError (DivByError a) m) => a -> [a] -> m [a] divByGeneric _ [] = return [] divByGeneric _ (0:_) = throwError DivBy0 divByGeneric _ (10:_) = throwError (ForbiddenDenominator 10) divByGeneric _ (20:_) = throwError (ForbiddenDenominator 20) divByGeneric numerator (denom:xs) = do next <- divByGeneric numerator xs return ((numerator `div` denom) : next)
Here, we needed to turn on the FlexibleContexts
language
extension in order to provide the type signature for
divByGeneric
. The divBy
function works exactly the same as
before. For divByGeneric
, we make divByError
a member of the
Error
class, by defining what happens when someone calls fail
(the strMsg
function). We also convert Right
to return
and
Left
to throwError
to enable this to be generic.
Exception handling is found in many programming languages,
including Haskell. It can be useful because, when a problem
occurs, it can provide an easy way of handling it, even if it
occurred several layers down through a chain of function calls.
With exceptions, it’s not necessary to check the return value of
every function call to check for errors, and take care to produce
a return value that reflects the error, as C programmers must do.
In Haskell, thanks to monads and the Either
and Maybe
types,
you can often achieve the same effects in pure code without the
need to use exceptions and exception handling.
Some problems—especially those involving I/O—call for working with
exceptions. In Haskell, exceptions may be thrown from any location
in the program. However, due to the unspecified evaluation order,
they can only be caught in the IO
monad. Haskell exception
handling doesn’t involve special syntax as it does in Python or
Java. Rather, the mechanisms to catch and handle exceptions
are—surprise—functions.
In the Control.Exception
module, various functions and types
relating to exceptions are defined. There is an Exception
type
defined there; all exceptions are of type Exception
. There are
also functions for catching and handling exceptions. Let’s start
by looking at try
, which has type
IO a -> IO (Either Exception a)
. This wraps an IO
action with
exception handling. If an exception was thrown, it will return a
Left
value with the exception; otherwise, a Right
value with
the original result. Let’s try this out in ghci
. We’ll first
trigger an unhandled exception, and then try to catch it.
ghci> :m Control.Exception
ghci> let x = 5 `div` 0
ghci> let y = 5 `div` 1
ghci> print x
*** Exception: divide by zero
ghci> print y
5
ghci> try (print x)
Left divide by zero
ghci> try (print y)
5
Right ()
Notice that no exception was thrown by the let
statements.
That’s to be expected due to lazy evaluation; the division by zero
won’t be attempted until it is demanded by the attempt to print
out x
. Also, notice that there were two lines of output from
try (print y)
. The first line was produced by print
, which
displayed the digit 5 on the terminal. The second was produced by
ghci
, which is showing you that print y
returned ()
and
didn’t throw an exception.
Now that you know how try
works, let’s try another experiment.
Let’s say we want to catch the result of try
for future
evaluation, so we can handle the result of division. Perhaps we
would do it like this:
ghci> result <- try (return x)
Right *** Exception: divide by zero
What happened here? Let’s try to piece it together, and illustrate with another attempt:
ghci> let z = undefined
ghci> try (print z)
Left Prelude.undefined
ghci> result <- try (return z)
Right *** Exception: Prelude.undefined
As before, assigning undefined
to z
was not a problem. The key
to this puzzle, and to the division puzzle, lies with lazy
evaluation. Specifically, it lies with return
, which does not
force the evaluation of its argument; it only wraps it up. So, the
result of try (return undefined)
would be Right undefined
.
Now, ghci
wants to display this result on the terminal. It gets
as far as printing out "Right "
, but you can’t print out
undefined
(or the result of division by zero). So when you see
the exception message, it’s coming from ghci
, not your program.
This is a key point. Let’s think about why our earlier example
worked and this one didn’t. Earlier, we put print x
inside
try
. Printing the value of something, of course, requires it to
be evaluated, so the exception was detected at the right place.
But simply using return
does not force evaluation. To solve this
problem, the Control.Exception
module defines the evaluate
function. It behaves just like return
, but forces its argument
to be evaluated immediately. Let’s try it:
ghci> let z = undefined
ghci> result <- try (evaluate z)
Left Prelude.undefined
ghci> result <- try (evaluate x)
Left divide by zero
There, that’s what was expected. This worked for both undefined
and our division by zero example.
Often, you may wish to perform one action if a piece of code
completes without an exception, and a different action otherwise.
For situations like this, there’s a function called handle
. This
function has type (Exception -> IO a) -> IO a -> IO a
. That is,
it takes two parameters: the first is a function to call in the
event there is an exception while performing the second. Here’s
one way we could use it:
ghci> :m Control.Exception
ghci> let x = 5 `div` 0
ghci> let y = 5 `div` 1
ghci> handle (\_ -> putStrLn "Error calculating result") (print x)
Error calculating result
ghci> handle (\_ -> putStrLn "Error calculating result") (print y)
5
This way, we can print out a nice message if there is an error in the calculations. It’s nicer than having the program crash with a division by zero error, for sure.
One problem with the above example is that it prints
"Error calculating result"
for any exception. There may have
been an exception other than a division by zero exception. For
instance, there may have been an error displaying the output, or
some other exception could have been thrown by the pure code.
There’s a function handleJust
for these situations. It lets you
specify a test to see whether you are interested in a given
exception. Let’s take a look:
#+CAPTION: hj1.hs import Control.Exception catchIt :: Exception -> Maybe () catchIt (ArithException DivideByZero) = Just () catchIt _ = Nothing handler :: () -> IO () handler _ = putStrLn "Caught error: divide by zero" safePrint :: Integer -> IO () safePrint x = handleJust catchIt handler (print x)
catchIt
defines a function that decides whether or not we’re
interested in a given exception. It returns Just
if so, and
Nothing
if not. Also, the value attached to Just
will be
passed to our handler. We can now use safePrint
nicely:
ghci> :l hj1.hs
[1 of 1] Compiling Main ( hj1.hs, interpreted )
Ok, modules loaded: Main.
ghci> let x = 5 `div` 0
ghci> let y = 5 `div` 1
ghci> safePrint x
Caught error: divide by zero
ghci> safePrint y
5
The Control.Exception
module also presents a number of functions
that we can use as part of the test in handleJust
to narrow down
the kinds of exceptions we care about. For instance, there is a
function arithExceptions
of type
Exception -> Maybe ArithException
that will pick out any
ArithException
, but ignore any other one. We could use it like
this:
import Control.Exception handler :: ArithException -> IO () handler e = putStrLn $ "Caught arithmetic error: " ++ show e safePrint :: Integer -> IO () safePrint x = handleJust arithExceptions handler (print x)
In this way, we can catch all types of ArithException
, but still
let other exceptions pass through unmodified and uncaught. We can
see it work like so:
ghci> :l hj2.hs
[1 of 1] Compiling Main ( hj2.hs, interpreted )
Ok, modules loaded: Main.
ghci> let x = 5 `div` 0
ghci> let y = 5 `div` 1
ghci> safePrint x
Caught arithmetic error: divide by zero
ghci> safePrint y
5
Of particular interest, you might notice the ioErrors
test,
which corresponds to the large class of I/O-related exceptions.
Perhaps the largest source of exceptions in any program is I/O.
All sorts of things can go wrong when dealing with the outside
world: disks can be full, networks can go down, or files can be
empty when you expect them to have data. In Haskell, an I/O
exception is just like any other exception in that can be
represented by the Exception
data type. On the other hand,
because there are so many types of I/O exceptions, a special
module—~System.IO.Error~ exists for dealing with them.
System.IO.Error
defines two functions: catch
and try
which,
like their counterparts in Control.Exception
, are used to deal
with exceptions. Unlike the Control.Exception
functions,
however, these functions will only trap I/O errors, and will pass
all other exceptions through uncaught. In Haskell, I/O errors all
have type IOError
, which is defined as the same as
IOException
.
Here, we use the System.IO.Error
version of try
to check
whether hGetLine
threw an IOError
. If it did, we use
isEOFError
(defined in System.IO.Error
) to see if the thrown
exception indicated that we reached the end of the file. If it
did, we exit the loop. If the exception was something else, we
call ioError
to re-throw it.
There are many such tests and ways to extract information from
IOError
defined in System.IO.Error
. We recommend that you
consult that page in the library reference when you need to know
about them.
Thus far, we have talked in detail about handling exceptions. There is another piece to the puzzle: throwing exceptions[fn:4]. In the examples we have visited so far in this chapter, the Haskell system throws exceptions for you. However, it is possible to throw any exception yourself. We’ll show you how.
You’ll notice that most of these functions appear to return a
value of type a
or IO a
. This means that the function can
appear to return a value of any type. In fact, because these
functions throw exceptions, they never “return” anything in the
normal sense. These return values let you use these functions in
various contexts where various different types are expected.
Let’s start our tour of ways to throw exceptions with the
functions in Control.Exception
. The most generic function is
throw
, which has type Exception -> a
. This function can throw
any Exception
, and can do so in a pure context. There is a
companion function throwIO
with type Exception -> IO a
that
throws an exception in the IO
monad. Both functions require an
Exception
to throw. You can craft an Exception
by hand, or
reuse an Exception
that was previously created.
There is also a function ioError
, which is defined identically
in both Control.Exception
and System.IO.Error
with type
IOError -> IO a
. This is used when you want to generate an
arbitrary I/O-related exception.
This makes use of two little-used Haskell modules: Data.Dynamic
and Data.Typeable
. We will not go into a great level of detail
on those modules here, but will give you the tools you need to
craft and use your own dynamic exception type.
In Chapter 21, /Using Databases/, you will see that the HDBC database
library uses dynamic exceptions to indicate errors from SQL
databases back to applications. Errors from database engines often
have three components: an integer that represents an error code, a
state, and a human-readable error message. We will build up our
own implementation of the HDBC SqlError
type here in this
chapter. Let’s start with the data structure representing the
error itself:
{-# LANGUAGE DeriveDataTypeable #-} import Data.Dynamic import Control.Exception data SqlError = SqlError {seState :: String, seNativeError :: Int, seErrorMsg :: String} deriving (Eq, Show, Read, Typeable)
By deriving the Typeable
type class, we’ve made this type
available for dynamically typed programming. In order for GHC to
automatically generate a Typeable
instance, we had to enable the
DeriveDataTypeable
language extension[fn:5].
Now, let’s define a catchSql
and a handleSql
that can be used
to catch an exception that is an SqlError
. Note that the regular
catch
and handle
functions cannot catch our SqlError
,
because it is not a type of Exception
.
{- | Execute the given IO action. If it raises a 'SqlError', then execute the supplied handler and return its return value. Otherwise, proceed as normal. -} catchSql :: IO a -> (SqlError -> IO a) -> IO a catchSql = catchDyn {- | Like 'catchSql', with the order of arguments reversed. -} handleSql :: (SqlError -> IO a) -> IO a -> IO a handleSql = flip catchSql
These functions are simply thin wrappers around catchDyn
, which
has type
Typeable exception => IO a -> (exception -> IO a) -> IO a
. We
here simply restrict the type of this so that it catches only SQL
exceptions.
Normally, when an exception is thrown, but not caught anywhere,
the program will crash and will display the exception to standard
error. With a dynamic exception, however, the system will not know
how to display this, so you will simply see an unhelpful “unknown
exception” message. We can provide a utility so that application
writers can simply say main = handleSqlError $ do ...
, and have
confidence that any exceptions thrown (in that thread) will be
displayed. Here’s how to write handleSqlError
:
{- | Catches 'SqlError's, and re-raises them as IO errors with fail. Useful if you don't care to catch SQL errors, but want to see a sane error message if one happens. One would often use this as a high-level wrapper around SQL calls. -} handleSqlError :: IO a -> IO a handleSqlError action = catchSql action handler where handler e = fail ("SQL error: " ++ show e)
Finally, let’s give you an example of how to throw an SqlError
as an exception. Here’s a function that will do just that:
throwSqlError :: String -> Int -> String -> a throwSqlError state nativeerror errormsg = throwDyn (SqlError state nativeerror errormsg) throwSqlErrorIO :: String -> Int -> String -> IO a throwSqlErrorIO state nativeerror errormsg = evaluate (throwSqlError state nativeerror errormsg)
This completes our dynamic exception support. That was a lot of
code, and you may not have needed that much, but we wanted to give
you an example of the dynamic exception itself and the utilities
that often go with it. In fact, these examples reflect almost
exactly what is present in the HDBC library. Let’s play with these
in ghci
for a bit:
ghci> :l dynexc.hs
[1 of 1] Compiling Main ( dynexc.hs, interpreted )
Ok, modules loaded: Main.
ghci> throwSqlErrorIO "state" 5 "error message"
*** Exception: (unknown)
ghci> handleSqlError $ throwSqlErrorIO "state" 5 "error message"
*** Exception: user error (SQL error: SqlError {seState = "state", seNativeError = 5, seErrorMsg = "error message"})
ghci> handleSqlError $ fail "other error"
*** Exception: user error (other error)
From this, you can see that ghci
doesn’t know how to display an
SQL error by itself. However, you can also see that our
handleSqlError
function helped out with that, but also passed
through other errors unmodified. Let’s finally try out a custom
handler:
ghci> handleSql (fail . seErrorMsg) (throwSqlErrorIO "state" 5 "my error")
*** Exception: user error (my error)
Here, we defined a custom error handler that threw a new
exception, consisting of the message in the seErrorMsg
field of
the SqlError
. You can see that it worked as intended.
- Take the
Either
example and made it work with laziness in the style of theMaybe
example.
Because we must catch exceptions in the IO
monad, if we try to
use them inside a monad, or in a stack of monad transformers,
we’ll get bounced out to the IO
monad. This is almost never what
we would actually like.
We defined a MaybeT
transformer in
the section called “Understanding monad transformers by building one”
but it is more useful as an aid to understanding than a
programming tool. Fortunately, a dedicated—and more useful—monad
transformer already exists: ErrorT
, which is defined in the
Control.Monad.Error
module.
The ErrorT
transformer lets us add exceptions to a monad, but it
uses its own special exception machinery, separate from that
provided the Control.Exception
module. It gives us some
interesting capabilities.
- If we stick with the
ErrorT
interfaces, we can both throw and catch exceptions within this monad. - Following the naming pattern of other monad transformers, the
execution function is named
runErrorT
. An uncaught ErrorT exception will stop propagating upwards when it reachesrunErrorT
. We will not be kicked out to theIO
monad. - We control the type our exceptions will have.
As with other mtl
monads, the interface that ErrorT
provides
is defined by a type class.
class (Monad m) => MonadError e m | m -> e where throwError :: e -- error to throw -> m a catchError :: m a -- action to execute -> (e -> m a) -- error handler -> m a
The type variable e
represents the error type we want to use.
Whatever our error type is, we must make it an instance of the
Error
type class.
class Error a where -- create an exception with no message noMsg :: a -- create an exception with a message strMsg :: String -> a
The strMsg
function is used by ErrorT
’s implementation of
fail
. It throws strMsg
as an exception, passing it the string
argument it received. As for noMsg
, it is used to provide an
mzero
implementation for the MonadPlus
type class.
To support the strMsg
and noMsg
functions, our ParseError
type will have a Chatty
constructor. This will be used as the
constructor if, for example, someone calls fail
in our monad.
One last piece of plumbing that we need to know about is the type
of the execution function runErrorT
.
ghci> :t runErrorT
runErrorT :: ErrorT e m a -> m (Either e a)
To illustrate the use of ErrorT
, let’s develop the bare bones of
a parsing library similar to Parsec.
{-# LANGUAGE GeneralizedNewtypeDeriving #-} import Control.Monad.Error import Control.Monad.State import qualified Data.ByteString.Char8 as B data ParseError = NumericOverflow | EndOfInput | Chatty String deriving (Eq, Ord, Show) instance Error ParseError where noMsg = Chatty "oh noes!" strMsg = Chatty
For our parser’s state, we will create a very small monad
transformer stack. A State
monad carries around the ByteString
to parse, and stacked on top is ErrorT
to provide error
handling.
newtype Parser a = P { runP :: ErrorT ParseError (State B.ByteString) a } deriving (Functor, Applicative, Monad, MonadError ParseError)
As usual, we have wrapped our monad stack in a newtype
. This
costs us nothing in performance, but adds type safety. We have
deliberately avoided deriving an instance of MonadState
B.ByteString
. This means that users of the Parser
monad will
not be able to use get
or put
to query or modify the parser’s
state. As a result, we force ourselves to do some manual lifting
to get at the State
monad in our stack. This is, however, very
easy to do.
liftP :: State B.ByteString a -> Parser a liftP m = P (lift m) satisfy :: (Char -> Bool) -> Parser Char satisfy p = do s <- liftP get case B.uncons s of Nothing -> throwError EndOfInput Just (c, s') | p c -> liftP (put s') >> return c | otherwise -> throwError (Chatty "satisfy failed")
The catchError
function is useful for tasks beyond simple error
handling. For instance, we can easily defang an exception, turning
it into a more friendly form.
optional :: Parser a -> Parser (Maybe a) optional p = (Just `liftM` p) `catchError` \_ -> return Nothing
Our execution function merely plugs together the various layers, and rearranges the result into a tidier form.
runParser :: Parser a -> B.ByteString -> Either ParseError (a, B.ByteString) runParser p bs = case runState (runErrorT (runP p)) bs of (Left err, _) -> Left err (Right r, bs) -> Right (r, bs)
If we load this into ghci
, we can put it through its paces.
ghci> :m +Data.Char
ghci> let p = satisfy isDigit
Loading package array-0.1.0.0 ... linking ... done.
Loading package bytestring-0.9.0.1 ... linking ... done.
Loading package mtl-1.1.0.0 ... linking ... done.
ghci> runParser p (B.pack "x")
Left (Chatty "satisfy failed")
ghci> runParser p (B.pack "9abc")
Right ('9',"abc")
ghci> runParser (optional p) (B.pack "x")
Right (Nothing,"x")
ghci> runParser (optional p) (B.pack "9a")
Right (Just '9',"a")
- Write a many parser, with type
Parser a -> Parser [a]
. It should apply a parser until it fails. - Use many to write an int parser, with type
Parser Int
. It should accept negative as well as positive integers. - Modify your int parser to throw a
NumericOverflow
exception if it detects a numeric overflow while parsing.
[fn:1] We’re using integral division here, so 50 / 8
shows as
6
instead of 6.25
. We’re not using floating-point arithmetic
in this example because division by zero with a Double
produces
the special value Infinity
rather than an error.
[fn:2] For an introduction to Maybe
, refer to
the section called “A more controlled approach”
[fn:3] For more information on Either
, refer to
the section called “Handling errors through API design”
[fn:4] In some other languages, throwing an exception is referred to as raising it.
[fn:5] It is possible to derive Typeable
instances by hand, but
that is cumbersome.