Throwing Exceptions: The Basics

Haskell is a pure, functional, strongly typed language. Unfortunately, this doesn't mean that nothing ever goes wrong or that there are no runtime errors. However, we can still use the type system in a few different ways to denote the specific problems that can occur. In the ideal case of error handling, I see an analogy to the state monad. Haskell "doesn't have mutable state". Except really it does…you just have to specify that mutable state is possible by placing your function in the State monad. Similarly, if we use particular functions, we often find that their types indicate the possibility that errors could arise in the computation.

The blog topic for June is "exceptional cases", so we're going to explore a wide variety of different ways that we can indicate runtime problems in Haskell and, more importantly, how we can write our code to catch these problems so our program doesn't suddenly crash in an unexpected way.

To start this journey, let's learn about "Exceptions" and how to throw them. A language like Java will have a class to represent the idea of exceptions:

class Exception {
  ...
}

This would serve as the base for other exception types. So you might define your own, like a "File" exception:

class FileException extends Exception {
}

Of course Haskell doesn't have classes or use inheritance in the same way. When it comes to inheritance, we rely on typeclasses. So Exception is a typeclass, not a data type.

class (Typeable e, Show e) => Exception e where
  ...

Notice that an exception type must be "showable". This makes sense, since the purpose of exceptions is to print them to the screen for output! They must also be Typeable, but virtually any type you'll make fulfills this constraint without you needing to even specify it.

There isn't a minimum definition for the Exception class. This means it is easy to define your own exception type. So as a first example, let's define an exception to work with lists. Certain list operations expect the list is non-empty, or that it has at least a certain number of elements. So we'll make an enumerated type with two constructors.

data ListException = ListIsEmpty | IndexNotFound
  deriving (Show)

We can derive the Show class, but we can't actually derive Exception under normal circumstances. However, since we don't need any functions, we just make a trivial instance.

data ListException = ListIsEmpty | NotEnoughElements
  deriving (Show)

instance Exception ListException

So what can we do with exceptions? Well the most important thing is that we can "throw" them to indicate the error has occurred. The throw function has a strange type if you look up the documentation:

throw :: forall (r :: RuntimeRep). forall (a :: TYPE r). forall e. Exception e => e -> a

This is a bit confusing, but to build a basic understanding, we can just look at the last part:

throw :: forall e. Exception e => e -> a

If we have an exception, we can use "throw" to trigger that exception and return any type. The a can be anything we want! All the magic stuff in the type signature essentially allows us to return this exception as "any type".

So for example, we can define a couple functions to operate on lists. These will have the "happy path" where we have enough elements, but they'll also have a failure mode. In the failure mode we'll throw the exception.

myHead :: [a] -> a
myHead [] = throw ListIsEmpty
myHead (a : _) = a

sum2Pairs :: (Num a) => [a] -> (a, a)
sum2Pairs (a : b : c : d : _) = (a + b, c + d)
sum2Pairs _ = throw NotEnoughElements

And when we use these functions, we can see how the exceptions occur:

>> myHead [4, 5]
4
>> myHead []
*** Exception: ListIsEmpty
>> sum2Pairs [5, 6, 7, 8, 9, 10]
(11, 15)
>> sum2Pairs [4, 5, 6]
Exception: NotEnoughElements

So even though our functions return different types, we can still use throw with our exception type on both of them.

You might also notice that our functions have pure type signatures! So using throw by itself in this way violates our notion of what pure functions ought to do. It's necessary to have this escape hatch in certain circumstances. However, we really want to avoid writing our code in this way if we possibly can.

In the coming weeks, we'll examine how to "catch" these kinds of exceptions so that our code still has some semblance of purity. To stay up to date with the latest Haskell news, make sure to subscribe to our monthly newsletter! This will keep you informed and, even better, give you access to our subscriber resources!

Previous
Previous

Catching What We’ve Thrown

Next
Next

Unit Testing User Interactions