Exception Type Details
A couple articles ago, we defined a basic exception type. Today, we'll go over some more details behind the way these exception types work. We'll consider how one might catch all exceptions, but also why this might not be a good idea.
Here's how we defined our exception type:
data ListException = ListIsEmpty | NotEnoughElements
deriving (Show)
instance Exception ListException
This indicates two different kinds of failures we might have when trying to process a list in a function. As long as we define or derive a Show
instance, we can simply say instance Exception
, and we'll be able to treat this type as an exception, because the class has no minimum definition.
So far, our example is a simple enumeration. But of course it's also possible to add data to these exception constructors. Let's suppose we want to know what function triggered the failure in the first type, and how many elements we expected and observed in the second type. Let's also define a custom Show
instance.
data ListException =
ListIsEmpty String |
NotEnoughElements String Int Int
instance Show ListException where
show (ListIsEmpty function) = "The function '" function ++ "' requires a non-empty list!"
show (NotEnoughElements function expected observed) =
"The function '" ++ function ++ "' expected " ++ show expected ++ " elements but only got " ++
show observed ++ " elements."
Now we can rewrite our functions to add this information.
myHead :: [a] -> a
myHead [] = throw (ListIsEmpty "myHead")
myHead (a : _) = a
sum2Pairs :: (Num a) => [a] -> (a, a)
sum2Pairs (a : b : c : d : _) = (a + b, c + d)
sum2Pairs input = throw (NotEnoughElements "sum2Pairs" 4 (length input))
And we can see this in action:
main :: IO ()
main = do
result0 <- try (evaluate (myHead []) :: IO (Either ListException Int)
print result0
result1 <- try (evaluate (sum2Pairs [2, 3, 4])) :: IO (Either ListException (Int, Int))
print result1
result2 <- try (evaluate (sum2Pairs [2, 3, 4, 5])) :: IO (Either ListException (Int, Int))
print result2
...
>> stack exec my-program
Left The function 'myHead' requires a non-empty list!
Left The function 'sum2Pairs' expected 4 elements but only got 3 elements.
Right (5, 9)
Now we didn't have to implement any custom functions to make our type an exception. But if we wanted to, we could! There are three functions we can override, but they all have appropriate default behaviors. The first of these functions is displayException
. You can use it to provide a second way to display the exception beyond the Show
instance, if you desire that for whatever reason. However, the Show
instance still has priority when the error is thrown by the system.
Let's try keeping the derived instance of Show
, but use our new function as the display message.
data ListException =
ListIsEmpty String |
NotEnoughElements String Int Int
deriving (Show)
instance Exception ListException where
displayMessage (ListIsEmpty function) = "The function '" function ++ "' requires a non-empty list!"
displayMessage (NotEnoughElements function expected observed) =
"The function '" ++ function ++ "' expected " ++ show expected ++ " elements but only got " ++
show observed ++ " elements."
We'll find that our program uses the Show
instance.
main :: IO ()
main = do
return (myHead ([] :: [Int]) >>= print
...
>> stack exec my-program
my-program: ListIsEmpty "myHead"
The other two functions in the definition require us to learn an additional concept: SomeException
.
class Exception e where
toException :: e -> SomeException
fromException :: SomeException -> Maybe e
The type SomeException
is essentially a wrapper type for all exceptions in Haskell. When the system receives and throws your exception, it is always wrapped as SomeException
under the hood. So in a way, this acts like the base Exception
class in a language like Java or Python. However, it acts like a wrapper instead of a "parent" class due to the lack of type-based inheritance in Haskell.
data SomeException e = forall e. Exception e => SomeException e
The two functions above would allow you to override how you transform your exception type back and forth with the SomeException
type. However, there's rarely any reason to override this behavior.
Now, since every exception is SomeException
, this means we could catch every possible exception with a handler function. Let's recall our previous example where we could catch a ListException
but not a file-based IO exception for opening a non-existent file:
main :: IO ()
main = do
handle handler $ readFile "does_not_exist.txt" >>= print
handle handler $ do
result <- return (sum2Pairs [2, 3, 4])
print result
where
handler :: ListException -> IO ()
handler e = print e
...
>> stack exec my-program
my-program: does_not_exit.txt: openFile: does not exist (No such file or directory)
If we modify our handler to take SomeException
instead of ListException
, it will catch both types!
main :: IO ()
main = do
...
where
handler :: SomeException -> IO ()
handler e = print e
...
>> stack exec my-program
does_not_exit.txt: openFile: does not exist (No such file or directory)
NotEnoughElements "sum2Pairs" 4 3
Typically, this is not a great idea. Haskell's type system allows us to be very specific with the errors we can catch, and we should take advantage of that. If you aren't anticipating a particular error, you shouldn't catch it. And if it pops up, you should adjust your program accordingly. However, catching "any" exception, logging it, and exiting gracefully as we just did IS a reasonable use case mentioned in the documentation on this subject.
The "proper" way to handle multiple exception types is to daisy chain handle
calls with different types like so:
main :: IO ()
main = handle ioHandler $ handle listHandler $ do
readFile "does_not_exist.txt" >>= print
result <- return (sum2Pairs [2, 3, 4])
print result
where
listHandler :: ListException -> IO ()
listHandler e = putStrLn $ "List exception: " ++ show e
ioHandler :: IOError -> IO ()
ioHandler e = putStrLn $ "IO Error: " ++ show e
In the next couple articles, we'll explore more ways to catch errors. Stay tuned! If you want access to our subscriber resources, you can sign up for our monthly newsletter!