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
throwIO
has 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 ===> 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 theIO
monad. ThethrowIO
variant should be used in preference to throw to raise an exception within theIO
monad because it guarantees ordering with respect to otherIO
operations, whereasthrow
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 show
ing 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