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!

Previous
Previous

Just Catching a Few Things

Next
Next

"Try"-ing It Out First