Stack Builders logo
Arrow icon Insights

Errors and Exceptions in Haskell

`*** Exception: Prelude.undefined`

*** Exception: Prelude.head: empty list

In Error versus Exception, Henning Thielemann makes a clear distinction between errors and exceptions in Haskell. Even though not all Haskellers make this distinction, it's useful to do so in order to talk about the most basic ways to handle computations that can go wrong and to discuss unsafe functions such as head, fromJust, and (!!).

On the one hand, an error is a programming mistake such as a division by zero, the head of an empty list, or a negative index. If we identify an error, we remove it. Thus, we don't handle errors, we simply fix them. In Haskell, we have error and undefined to cause such errors and terminate execution of the program.

On the other hand, an exception is something that can go wrong or that simply doesn't follow a rule but can happen. For example, reading a file that does not exist or that is not readable. If we identify a possible exception, we handle it, and not doing so would be an error. In Haskell, we have pure (Maybe and Either, for instance) and impure ways to handle exceptions.

A basic example of an error in Haskell is trying to get the head of an empty list using the head function as defined in GHC.List:

head :: [a] -> a
head []    = error "head: empty list"
head (x:_) = x

One way to distinguish an error from an exception is to think in terms of contracts and preconditions. In this case, there's a precondition in the documentation of the head function: the list must be nonempty. This means that the first equation of head is supposed to be dead or unreachable code. This way, if we are sure that a list has at least one element, we can extract its head:

ghci> head [104,97,115,107,101,108,108]
104

Of course, the type signature of the head function says nothing about such contract, which means that there's nothing stopping us from applying it to an empty list and therefore breaking the rules:

ghci> head []
*** Exception: head: empty list

As a comment in the definition of the fromJust function in the Data.Maybe module says, "yuck."

Even if trying to get the head of an empty list using head is an error, it's unsafe to do so. We can certainly treat it as an exception and handle it with the Maybe data type:

data Maybe a = Nothing | Just a

In terms of exceptions, the Maybe type represents a computation that can fail (in the case of a Nothing).

Let's define a safe version of the head function:

maybeHead :: [a] -> Maybe a
maybeHead []    = Nothing
maybeHead (x:_) = Just x

The safety of the maybeHead function relies on its type signature. We know that applying the function to a list can succeed:

ghci> maybeHead [104,97,115,107,101,108,108]
Just 104

Or fail:

ghci> maybeHead []
Nothing

A similar example is the fromJust function, which extracts the element out of a Just:

fromJust :: Maybe a -> a
fromJust Nothing  = error "Maybe.fromJust: Nothing" -- yuck
fromJust (Just x) = x

Again, it's an error to apply fromJust to a Nothing, but there's nothing stopping us from doing it. It's best if we use a safe function such as fromMaybe, which takes a default value:

fromMaybe :: a -> Maybe a -> a
fromMaybe d mx =
  case mx of
    Nothing -> d
    Just x  -> x

Yet another example is the lookup function, which looks up a key in an association list or dictionary:

lookup :: Eq a => a -> [(a,b)] -> Maybe b
lookup _   []          = Nothing
lookup key ((x,y):xys)
  | key == x           = Just y
  | otherwise          = lookup key xys

In this case, applying lookup to an empty list or to a list which doesn't contain the key we're looking for is not an error but an exception, and the type signature of the function clearly specifies that it can go wrong:

ghci> lookup 1 (zip [1..] [104,97,115,107,101,108,108])
Just 104
ghci> lookup 1 []
Nothing
ghci> lookup (-1) (zip [1..] [104,97,115,107,101,108,108])
Nothing

Now, let's consider the elemAt (or (!!)) function, which is a list index operator:

elemAt :: [a] -> Int -> a
elemAt xs     n | n < 0 = error "elemAt: negative index"
elemAt []     _         = error "elemAt: index too large"
elemAt (x:_)  0         = x
elemAt (_:xs) n         = elemAt xs (n - 1)

This function has two preconditions: the index must be nonnegative and less than the length of the list. For example:

ghci> elemAt [104,97,115,107,101,108,108] 0
104
ghci> elemAt [104,97,115,107,101,108,108] (-8)
*** Exception: elemAt: negative index
ghci> elemAt [104,97,115,107,101,108,108] 8
*** Exception: elemAt: index too large

The elemAt function is as unsafe as head and fromJust in that its type signature tells us nothing about the possibility of failure. We could define a safe version using Maybe, but now we have two different errors and it would be nice to provide additional information about what went wrong, which we can accomplish with the Either data type:

data Either a b = Left a | Right b

In terms of exceptions, a Left represents failure and a Right represents success.

Here's a safe version of elemAt using strings for exceptions:

eitherElemAt :: [a] -> Int -> Either String a
eitherElemAt _      n | n < 0 = Left "elemAt: negative index"
eitherElemAt []     _         = Left "elemAt: index too large"
eitherElemAt (x:_)  0         = Right x
eitherElemAt (_:xs) n         = eitherElemAt xs (n - 1)

We can safely apply this version of elemAt to the lists and indexes we used before:

ghci> eitherElemAt [104,97,115,107,101,108,108] 0
Right 104
ghci> eitherElemAt [104,97,115,107,101,108,108] 8
Left "elemAt: index too large"
ghci> eitherElemAt [104,97,115,107,101,108,108] (-8)
Left "elemAt: negative index"

We know that there are only two things that can go wrong with elemAt and that means that a String is too general for representing failure in this case. We can be more specific by defining our own data type and moving the error strings to the Show instance:

data ElemAtError
  = IndexTooLarge
  | NegativeIndex

instance Show ElemAtError where
  show IndexTooLarge = "elemAt: index too large"
  show NegativeIndex = "elemAt: negative index"

And we can use this data type for exceptions in another safe version of elemAt:

errorElemAt :: [a] -> Int -> Either ElemAtError a
errorElemAt _      n | n < 0 = Left NegativeIndex
errorElemAt []     _         = Left IndexTooLarge
errorElemAt (x:_)  0         = Right x
errorElemAt (_:xs) n         = errorElemAt xs (n - 1)

Which we can safely apply to the same lists and indexes as before:

ghci> errorElemAt [104,97,115,107,101,108,108] 0
Right 104
ghci> errorElemAt [104,97,115,107,101,108,108] 8
Left elemAt: index too large
ghci> errorElemAt [104,97,115,107,101,108,108] (-8)
Left elemAt: negative index

It might be confusing to call our custom data type ElemAtError instead of ElemAtException, but perhaps it's a better name for reflecting that we're treating errors as exceptions for the sake of safety.

It's even more confusing once we figure out that the implementation of error is actually raising an exception, but a very general one. Even then, we can be more specific about the exception that gets thrown by making our ElemAtError type an instance of Exception, as follows:

instance Exception ElemAtError

Instead of calling error, we can now throw the constructors of our ElemAtError data type if there's a problem with the index:

exceptionElemAt :: [a] -> Int -> a
exceptionElemAt _      n | n < 0 = throw NegativeIndex
exceptionElemAt []     _         = throw IndexTooLarge
exceptionElemAt (x:_)  0         = x
exceptionElemAt (_:xs) n         = exceptionElemAt xs (n - 1)

Which is very similar to what we had with the original elemAt function:

ghci> exceptionElemAt [104,97,115,107,101,108,108] 0
104
ghci> exceptionElemAt [104,97,115,107,101,108,108] 8
*** Exception: elemAt: index too large
ghci> exceptionElemAt [104,97,115,107,101,108,108] (-8)
*** Exception: elemAt: negative index

But this time we can use the try function from Control.Exception, which takes an action and returns either the result of that action or an exception:

try :: Exception e => IO a -> IO (Either e a)

Since we're using a very specific type to represent things that can go wrong with the elemAt function, we can also be very specific about what to do in case that something actually goes wrong:

tryExceptionElemAt :: Show a => [a] -> Int -> IO ()
tryExceptionElemAt xs n = do
  eitherExceptionElemAt <- try (evaluate (exceptionElemAt xs n))
  case eitherExceptionElemAt of
    Left  IndexTooLarge -> print IndexTooLarge
    Left  NegativeIndex -> print NegativeIndex
    Right elemAt        -> print elemAt

Given a list xs and an index n, we try to get the element at that position using exceptionElemAt, and then use a case expression to pattern match against the Either returned by try. In this case, we're simply printing the error or the result, which is not very useful.

For now, we can try our lists and indexes, and see that we succesfully handled everything that could go wrong:

ghci> tryExceptionElemAt [104,97,115,107,101,108,108] 0
104
ghci> tryExceptionElemAt [104,97,115,107,101,108,108] 8
elemAt: index too large
ghci> tryExceptionElemAt [104,97,115,107,101,108,108] (-8)
elemAt: negative index

It's obviously better to use a safe function such as eitherElemAt or errorElemAt, but exceptionElemAt gives us a good idea of how to raise and catch exceptions in Haskell.

Finally, let's consider reading a file using the readFile function, which could fail for two reasons: the file doesn't exist or the user doesn't have enough permissions to read it. We'll use the tryJust function, which is like try but takes a handler that allows us to select which exceptions are caught:

tryJust :: Exception e => (e -> Maybe b) -> IO a -> IO (Either b a)

Here's a function that tries to read a given file:

tryJustReadFile :: FilePath -> IO ()
tryJustReadFile filePath = do
  eitherExceptionFile <- tryJust handleReadFile (readFile filePath)
  case eitherExceptionFile of
   Left  er   -> putStrLn er
   Right file -> putStrLn file
  where
    handleReadFile :: IOError -> Maybe String
    handleReadFile er
      | isDoesNotExistError er = Just "readFile: does not exist"
      | isPermissionError   er = Just "readFile: permission denied"
      | otherwise              = Nothing

Given a file name, we try to read it with readFile and choose the exceptions we're going to handle with the handleReadFile function. If the result of trying to read the file is a Left, we print the exception message. If it's a Right, we print the contents of the file. The handleReadFile function returns appropriate messages for errors that satisfy ioDoesNotExistError or isPermissionError (which are exceptions in System.IO.Error), and ignores any other exception.

Let's try to read the contents of a file called haskell before creating it:

ghci> tryJustReadFile "haskell"
readFile: does not exist

We don't get an *** Exception because we handled the exception and decided to simply print the exception message.

If we create the file and add something to it, and then try to read its contents, we get the expected result:

$ echo [104,97,115,107,101,108,108] > haskell
ghci> tryJustReadFile "haskell"
[104,97,115,107,101,108,108]

And if we don't have permissions to read the file, we get the expected exception message:

$ chmod -r haskell
ghci> tryJustReadFile "haskell"
readFile: permission denied

For more information about errors and exceptions in Haskell, see the Error Handling chapter in Real World Haskell or the Control.Exception module in the base package.

Published on: Aug. 3, 2015
Last updated: Dec. 21, 2024

Written by:

Software Developer
Juan Pedro Villa

Subscribe to our blog

Join our community and get the latest articles, tips, and insights delivered straight to your inbox. Don’t miss it – subscribe now and be part of the conversation!
We care about your data. Check out our Privacy Policy.