Skip to content

Instantly share code, notes, and snippets.

@therewillbecode
Last active April 25, 2020 13:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save therewillbecode/ac4b524d2f31b5814b447fbc4e01b421 to your computer and use it in GitHub Desktop.
Save therewillbecode/ac4b524d2f31b5814b447fbc4e01b421 to your computer and use it in GitHub Desktop.
Exceptions in Haskell Notes

Exceptions

All code that runs in IO can experience exceptions of any type.

Remember you can only catch an exception in IO. But it is possible to throw exceptions from pure code. An example of this is error.

Throw doesn't throw until forced

Due to laziness the order of evaluation of expressions can be modified by the compiler. This means that throw can return an exception which is inside a thunk that is evaluated at the wrong time and thus is evaluated outside of your catch clause.

Prefer throwIO to throw for throwing an exception as throwIO piggybacks the strong ordering guarantees that the RTS provides for executing IO actions. Otherwise you are likely to run in to issues where the exception is evaluated at the wrong time due to laziness.

Prefer throwIO to throw

Since exceptions are only thrown when they are evaluated to the level of the exception we prefer the use of throwIO to throw. throwIO ties the raising of the exception to execution. Evaluating the IO action won't raise now. Only executing the IO action will raise the exception. The benefit of this over just throw is that the runtime system gives special ordering guarantees for IO actions.

This means that our exception will always be raised at the correct time and cannot hide inside unevaluated expressions since the raising of the exception isn'rt tied to evaluation anymore. We know the exception raising is now independent of evaluation with throwIO as even if you evaluate the IO value fully the exception will not be raised.

Think about the IO action 'readFile "non_existent.txt"'. Evaluating it cannot raise an exception. It only throws when it actually the IO action is executed.

If you use "throwIO :: Exception e => e -> IO a" it guarantees that the exception is raised before the "IO a" finishes. This guarantees it can't escape from the "IO a" part of catch (because catch only terminates after the first argument finishes and throwIO guarantees raising the exception before then)

evaluate

evaluate . force can be used to flush out exceptions from a value by fully evaluating the expreession to normal form. This is wasteful as the full evaluation will take place even if there is no exception present and thus throwIO should be preferred. the exception at a point in time where we know it will be catched.

So far we have only takes about synchronous expressions.

Async functions are raised as a result of an external event to a given thread. They are thrown from one thread to another

and therefore can happen at any time.

To identify an async exception look at the way it is thrown. Async exceptions use throwTo whereas sync exceptions use throwIO or throw

Why catch exceptions

There are two reasons to catch an exception

  1. Cleanup - you catch, perform the cleanup action and rethrow. This is ok for an async exception.
  2. Recover - Catch the exceptions and continue something else without rethrowing. This is NOT OK for async exceptions.

You cannot recover from an asynchronous exception. You shouldn't try. You catch async exceptions, clean up and then rethrow the exception.

Unlike sync exceptions, the async bretherin are rethrown by exception handling mechanisms

Exception handlers work differently for async functions in that they will rethrow them at some point since you can't recover from them. The functions , catch, try and handle immediately rethrow async exceptions and not allow you to recover.

Whereas bracket, onException and finally will allow you to clean up from the exception before they rethrow the async exception. These functions don't actually give you access to the exception itself, they just call your clean up function.

Antipatterns

Avoid low level masking whenever possible

Masking is complicated so try and use higher level functions. You can use helper functions 99% of the time.

The catch'em all anti-pattern

When using catch be granular about the exact exception you are catching. There are loads of possible exceptions which can occur. For example you might get a UserInterrupt exception a result of the user hitting ^c in an attempt to kill the running program. Do you really want to handle this case? Probably only if you are cleaning up scarce resources.

Most of the time catching all exceptions is the right thing to do if you are performing clean up. A case where this also makes sense is at the top level of your program if you want to print the exception and then exit. To catch all exceptions you would use the SomeExceptionType which covers all possible exceptions. catch f (\e -> ... (e :: SomeException) ...)

Cookbook

Handling different exceptions in the same catch clause

f = expr `catches` [Handler (\ (ex :: ArithException) -> handleArith ex),
                    Handler (\ (ex :: IOException)    -> handleIO    ex)]```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment