"Try"-ing It Out First

Earlier this week we explored how to "catch" exceptions using the functions catch and handle. Today we'll learn a couple new tools for this task. The first function we'll look at is try, but in order to really use it, we'll also have to use evaluate.

Like catch, we can use try to turn our exception into a computation that our program can process and react to gracefully. However, instead of taking an exception handler, this function will simply return the exception using an Either value.

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

The computation produces the result type a, but could throw an exception e. So we return the type Either e a. All this must be done in the IO monad, like we saw with catch.

Let's recall our previous approach to catching exceptions. Since we had a pure function (sum2Pairs) that could throw the exception, we would use return in order to move it into the IO monad to use catch. We also needed an explicit type signature on our handler function so that our program knows what exceptions it is trying to catch:

main :: IO ()
main = do
  catch (return (sum2Pairs [2, 3, 4]) >>= print) handler
  catch (return (sum2Pairs [2, 3, 4, 5]) >>= print) handler
  where
    handler :: ListException -> IO ()
    handler e = print e

Let's try to substitute try in for these expressions. Once again, we'll explicitly annotate the resulting value with the exception type.

main :: IO ()
main = do
  result1 <- try (return (sum2Pairs [2, 3, 4])) :: IO (Either ListException (Int, Int))
  print result1
  result2 <- try (return (sum2Pairs [2, 3, 4, 5])) :: IO (Either ListException (Int, Int))
  print result2

However, this doesn't work the way we want! Our program crashes on the exceptional case!

my-program: NotEnoughElements

The reason for this lies in Haskell's laziness. The exceptional computation doesn't actually occur until we "need" the value, which is when the print statement happens. But by delaying the computation, our program loses the try context. We can try to wrap the print statement into our "try" block, but it makes our program unnecessarily complicated.

Instead, we have a different tool to help us. This is the evaluate function.

evaluate :: a -> IO a

At first glance, this seems to be the same type as return! It takes a pure value and wraps it in the IO monad. However, it will take care of "evaluating" our expression in a strict (non-lazy) manner. So the computation will occur when we need it to, and we can use "try". If we change our above implementation by swapping evaluate for return, then it works!

main :: IO ()
main = do
  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

...

Left NotEnoughElements
Right (5, 9)

So now we have a more reliable way of turning our "pure" computations into expressions where we can catch their exceptions. In the next couple articles, we'll focus some more on what we can do with exceptional data types. Until then, make sure to subscribe to our monthly newsletter so you can stay up to date with the latest news and get access to our subscriber resources!

Previous
Previous

Exception Type Details

Next
Next

Catching What We’ve Thrown