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
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
throwIOhas a type that is an instance of the type ofthrow, the two functions are subtly different:throw e `seq` x ===> throw e throwIO e `seq` x ===> xThe first example will cause the exception
eto be raised, whereas the second one won't. In fact,throwIOwill only cause an exception to be raised when it is used within theIOmonad. ThethrowIOvariant should be used in preference to throw to raise an exception within theIOmonad because it guarantees ordering with respect to otherIOoperations, whereasthrowdoes 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With