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!