Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How the `onException` behaves

finally and onException are two functions from the module Control.Exception, which have the same signature but behave differently. Here is the document. For finally, it says:

finally 
:: IO a  -- computation to run first
-> IO b  -- computation to run afterward (even if an exception was raised)
-> IO a

, while for onException, it says:

Like finally, but only performs the final action if there was an exception raised by the computation.

So I do the following test:

ghci> finally (return $ div 4 2) (putStrLn "Oops!")
Oops!
2
ghci> finally (return $ div 4 0) (putStrLn "Oops!")
Oops!
*** Exception: divide by zero

which acts as expected.

However, onException does not:

ghci> onException (return $ div 4 2) (putStrLn "Oops!")
2
ghci> onException (return $ div 4 0) (putStrLn "Oops!")  -- does not act as expected
*** Exception: divide by zero

As describe above, onException only performs the final action if an exception was raised, but the example above shows that onException does not perform the final action, i.e. putStrLn "Oops!" when an exception raised.

After checking the source code for onException, I try the test as follow:

ghci> throwIO (SomeException DivideByZero) `catch` \e -> do {_ <- putStrLn "Oops!"; throwIO (e :: SomeException)}
Oops!
*** Exception: divide by zero
ghci> onException (throwIO (SomeException DivideByZero)) (putStrLn "Oops!")
Oops!
*** Exception: divide by zero

As can be seen, when an exception raised explicitly, the final action was performed.

So the question is return $ div 4 0 really throws an exception, but why onException (return $ div 4 0) (putStrLn "Oops!") does not perform the final action putStrLn "Oops!"? What am I missing? And how the exception was performed?

ghci> throwIO (SomeException DivideByZero)
*** Exception: divide by zero
ghci> (return $ div 4 0) :: IO Int
*** Exception: divide by zero
like image 270
Z-Y.L Avatar asked Dec 23 '22 04:12

Z-Y.L


1 Answers

You’ve been bitten by lazy evaluation.

One of the key guarantees of throwIO is that it guarantees when the exception will be raised with respect to the execution of other IO actions. From the documentation:

Although throwIO has a type that is an instance of the type of throw, the two functions are subtly different:

throw e   `seq` x  ===> throw e
throwIO e `seq` x  ===> x

The first example will cause the exception e to be raised, whereas the second one won't. In fact, throwIO will only cause an exception to be raised when it is used within the IO monad. The throwIO variant should be used in preference to throw to raise an exception within the IO monad because it guarantees ordering with respect to other IO operations, whereas throw does not.

This means that, when the throwIO e action is executed (not merely evaluated!) as a part of the execution of the action produced by onException, it is guaranteed to actually raise an exception. Since the exception is raised within the dynamic extent of the execution of the exception handler, the exception is detected, and the handler function is executed.

However, when you write return e, the action it produces does not evaluate e to WHNF when it is executed, and e is only evaluated if/when the action’s result is itself evaluated. By the time the div 4 0 expression is forced by GHCi by showing the action’s result, control has left the dynamic extent of the execution of onException, and the handler it installed is no longer on the stack. The exception is raised, but it is raised too late.

To get the behavior you want, it’s critical to ensure you evaluate div 4 0 as a part of your action’s execution, and not a moment before or after. This is what the evaluate function from Control.Exception is for. It evaluates its argument to WHNF as a part of the execution of the IO action itself, guaranteeing that any exceptions raised as part of that evaluation will be detected by a surrounding exception handler:

ghci> onException (evaluate $ div 4 0) (putStrLn "Oops!")
Oops!
*** Exception: divide by zero

The moral: when handling exceptions in Haskell, be very careful about when things are evaluated to ensure the exception is raised within the dynamic extent of your exception handler and is not deferred due to lazy evaluation.

like image 71
Alexis King Avatar answered Jan 01 '23 22:01

Alexis King