Introduction to Programming

Exception Handling in Haskell


Material adapted from

The Haskell School of Expression: Learning Functional Programming Through Multimedia
by Paul Hudak
Cambridge University Press (2000) pp 239-240.

So far, we have avoided the issue of errors during IO operations. What would happen, for example, if getChar encountered the end of a file? We certainly don't want the whole program to terminate when this happens, such as with run-time errors in Haskell. In other words, we would like a way to recover from anomalous conditions, which we prefer to call exceptions rather than errors. Recovering from an exception is called exception handling. In Haskell no special syntax or semantics is needed to achieve this; all we need is a few more IO commands.

Exceptions themselves have type IOError. Among the operations allowed on this type, there are first a collection of predicates that can be used to test for a particular kind of exception. For example, this predicate:

    isEOFError :: IOError -> Bool
determines whether an end-of-file exception has occurred. (Note:)

More importantly, there is a function catch, which does the actual exception handling. The first argument to catch is an IO action that we are attempting to execute. The second argument is an exception handler of type IOError -> IO a.

    catch :: IO a -> (IOError -> IO a) -> IO a
The metaphor to grasp here is this: In the command

    catch command handler
any exception that occurs in command (which may generate a long sequence of actions, perhaps even infinite) gets "thrown" outward, to be "caught" by the exception handler handler. Control is effectively transferred to the handler by applying it to the IOError that occurred. If command succeeds, the handler is ignored.

For example, this version of getChar returns a newline character if any kind of exception is encountered:

   getChar1 :: IO Char
   getChar1 =  catch getChar (\e -> return '\n')
However, this is rather crude because it treats all exceptions in the same manner. If only the end-of-file exception is to be recognized, the IOError value must be queried:

   getChar1 :: IO Char
   getChar1 =  catch getChar (\e -> if isEOFError e
                                       then return '\n'
                                       else ioError e)
The ioError function used here "throws" the exception upward to the next exception handler. In other words, nested calls to catch are permitted, and produce nested exception handlers. The function ioError may be called from within a normal action sequence or from within an exception handler as in the getChar1 example above.

Using getChar1 we can redefine getLine to demonstrate the use of nested handlers:

   getLine1 :: IO String
   getLine1 = catch getLine2 (\err -> return ("Error: "++(show err))
              where getLine2 = do c <- getChar1
                                  if c == '\n'
                                     then return ""
                                     else do l <- getLine1
                                             return (c:l)
Note how "looping" is achieved simply by a recursive call to getline1. Nested exception handlers allow getChar1 to catch the end-of-file exception while any other exception results in a string starting with "Error: " to be returned from getLine1.

If you do not provide an exception handler for your program, or fail to catch a particular exception that occurs, Haskell invokes a default exception handler that prints out the exception and terminates your program.


Note:

You might wonder why the Haskell designers did not just define IOError as a datatype that enumerated all possible IO errors. The main reason is that some implementations may support different kinds of such errors, and datatypes in Haskell are not extensible. Although the predicate approach is more cumbersome to use, it is easy to add support for a new IO error by simply including an additional predicate.

Back to main text.


Last updated 20 November, 2003