Functional Programming

Functional Programming

Functional Programming

Jun 29, 2016

Announce: safe-exceptions, for async exception safety

Announce: safe-exceptions, for async exception safety

Announce: safe-exceptions, for async exception safety

This blog post is an initial announcement of a new package,

safe-exceptions (and Github repo).

This is a follow up to a number of comments I made in last week's blog

post. To quote the README:

Safe, consistent, and easy exception handling Runtime exceptions - as exposed in base by the Control.Exception module - have long been an

intimidating part of the Haskell ecosystem. This package, and this

README for the package, are intended to overcome this. By providing

an API that encourages best practices, and explaining the corner

cases clearly, the hope is to turn what was previously something

scary into an aspect of Haskell everyone feels safe using.

This is an initial release of the package. I fully expect the

library to expand in the near future, and in particular there are

two open issues for decisions that need to be made in the short

term. I'm releasing the package in its current state since:

  1. I think it's useful as-is

  2. I'm hoping to get feedback on how to improve it

On the second point, I've created

a survey to get feedback on the interruptible/uninterruptible

issue and the throw naming issue. Both are described

in this blog post.

I'm hoping this library can bring some sanity and comfort to

people dealing with IO and wanting to ensure proper

exception handling! Following is the content of the README, which

can also be read on Github.

Goals

This package provides additional safety and simplicity versus

Control.Exception by having its functions recognize

the difference between synchronous and asynchronous exceptions. As

described below, synchronous exceptions are treated as

recoverable, allowing you to catch and handle them as well

as clean up after them, whereas asynchronous exceptions can only be

cleaned up after. In particular, this library prevents you from

making the following mistakes:

  • Catching and swallowing an asynchronous exception

  • Throwing an asynchronous exception synchronously

  • Throwing a synchronous exception asynchronously

  • Swallowing asynchronous exceptions via failing cleanup handlers

Quickstart

This section is intended to give you the bare minimum

information to use this library (and Haskell runtime exceptions in

general) correctly.

  • Import the Control.Exception.Safe module. Do not import Control.Exception itself, which

    lacks the safety guarantees that this library adds. Same applies to

    Control.Monad.Catch.

  • If something can go wrong in your function, you can report this with the throw. (For compatible naming, there are synonyms for this of throwIO and throwM.)

  • If you want to catch a specific type of exception, use catch, handle, or try.

  • If you want to recover from anything that may go wrong in a function, use catchAny, handleAny, or tryAny.

  • If you want to launch separate threads and kill them externally, you should use the async package.

  • Unless you really know what you're doing, avoid the following

    functions:

    • catchAsync

    • handleAsync

    • tryAsync

    • impureThrow

    • throwTo

  • If you need to perform some allocation or cleanup of resources, use one of the following functions (and don't use the catch/handle/try family of

    functions):

    • onException

    • withException

    • bracket

    • bracket_

    • finally

    • bracketOnError

    • bracketOnError_

Hopefully this will be able to get you up-and-running

quickly.

Request to readers: if there are specific workflows that

you're unsure of how to accomplish with this library, please ask so

we can develop a more full-fledged cookbook as a companion to this

file.

Terminology

We're going to define three different versions of exceptions.

Note that these definitions are based on how the exception is thrown, not based on what the exception itself is:

  • Synchronous exceptions are generated by the current

    thread. What's important about these is that we generally want to

    be able to recover from them. For example, if you try to read from

    a file, and the file doesn't exist, you may wish to use some

    default value instead of having your program exit, or perhaps

    prompt the user for a different file location.

  • Asynchronous exceptions are thrown by either a different

    user thread, or by the runtime system itself. For example, in the

    async package, race will kill the

    longer-running thread with an asynchronous exception. Similarly,

    the timeout function will kill an action which has run

    for too long. And the runtime system will kill threads which appear

    to be deadlocked on MVars or STM

    actions.

    In contrast to synchronous exceptions, we almost never want to

    recover from asynchronous exceptions. In fact, this is a common

    mistake in Haskell code, and from what I've seen has been the

    largest source of confusion and concern amongst users when it comes

    to Haskell's runtime exception system.

  • Impure exceptions are hidden inside a pure value, and

    exposed by forcing evaluation of that value. Examples are

    error, undefined, and impureThrow. Additionally, incomplete pattern matches

    can generate impure exceptions. Ultimately, when these pure values

    are forced and the exception is exposed, it is thrown as a

    synchronous exception.

    Since they are ultimately thrown as synchronous exceptions, when

    it comes to handling them, we want to treat them in all ways like

    synchronous exceptions. Based on the comments above, that means we

    want to be able to recover from impure exceptions.

Why catch asynchronous exceptions?

If we never want to be able to recover from asynchronous

exceptions, why do we want to be able to catch them at all? The

answer is for resource cleanup. For both sync and async

exceptions, we would like to be able to acquire resources - like

file descriptors - and register a cleanup function which is

guaranteed to be run. This is exemplified by functions like

bracket and withFile.

So to summarize:

  • All synchronous exceptions should be recoverable

  • All asynchronous exceptions should not be recoverable

  • In both cases, cleanup code needs to work reliably

Determining sync vs async

Unfortunately, GHC's runtime system provides no way to determine

if an exception was thrown synchronously or asynchronously, but

this information is vitally important. There are two general

approaches to dealing with this:

  • Run an action in a separate thread, don't give that thread's ID

    to anyone else, and assume that any exception that kills it is a

    synchronous exception. This approach is covered in the School of

    Haskell article catching all exceptions, and is provided by the enclosed-exceptions package.

  • Make assumptions based on the type of an exception, assuming

    that certain exception types are only thrown synchronously and

    certain only asynchronously.

Both of these approaches have downsides. For the downsides of

the type-based approach, see the caveats section at the end. The

problems with the first are more interesting to us here:

  • It's much more expensive to fork a thread every time we want to deal with exceptions

  • It's not fully reliable: it's possible for the thread ID of the

    forked thread to leak somewhere, or the runtime system to send it

    an async exception

  • While this works for actions living in IO, it gets

    trickier for pure functions and monad transformer stacks. The

    latter issue is solved via monad-control and the exceptions

    packages. The former issue, however, means that it's impossible to

    provide a universal interface for failure for pure and impure

    actions. This may seem esoteric, and if so, don't worry about it

    too much.

Therefore, this package takes the approach of trusting type

information to determine if an exception is asynchronous or

synchronous. The details are less interesting to a user, but the

basics are: we leverage the extensible extension system in GHC and

state that any extension type which is a child of

SomeAsyncException is an async exception. All other

exception types are assumed to be synchronous.

Handling of sync vs async exceptions

Once we're able to distinguish between sync and async

exceptions, and we know our goals with sync vs async, how we handle

things is pretty straightforward:

  • If the user is trying to install a cleanup function (such as with bracket or finally), we don't care

    if the exception is sync or async: call the cleanup function and

    then rethrow the exception.

  • If the user is trying to catch an exception and recover from

    it, only catch sync exceptions and immediately rethrow async

    exceptions.

With this explanation, it's useful to consider async exceptions

as "stronger" or more severe than sync exceptions, as the next

section will demonstrate.

Exceptions in cleanup code

One annoying corner case is: what happens if, when running a

cleanup function after an exception was thrown, the cleanup

function itself throws an exception. For this, we'll consider action `onException` cleanup. There are four

different possibilities:

  • action threw sync, cleanup threw sync

  • action threw sync, cleanup threw async

  • action threw async, cleanup threw sync

  • action threw async, cleanup threw async

Our guiding principle is: we cannot hide a more severe exception

with a less severe exception. For example, if action threw a sync exception, and then cleanup threw an

async exception, it would be a mistake to rethrow the sync

exception thrown by action, since it would allow the

user to recover when that is not desired.

Therefore, this library will always throw an async exception if

either the action or cleanup thows an async exception. Other than

that, the behavior is currently undefined as to which of the two

exceptions will be thrown. The library reserves the right to throw

away either of the two thrown exceptions, or generate a new

exception value completely.

Typeclasses

The exceptions package provides an abstraction for throwing, catching, and

cleaning up from exceptions for many different monads. This library

leverages those type classes to generalize our functions.

Naming

There are a few choices of naming that differ from the base

libraries:

  • throw in this library is for synchronously

    throwing within a monad, as opposed to in base where

    throwIO serves this purpose and throw is for impure throwing. This library provides impureThrow

    for the latter case, and also provides convenience synonyms

    throwIO and throwM for throw.

  • The catch function in this package will not catch async exceptions. Please use catchAsync if you really

    want to catch those, though it's usually better to use a function

    like bracket or withException which ensure that the thrown exception is rethrown.

Caveats

Let's talk about the caveats to keep in mind when using this

library.

Checked vs unchecked

There is a big debate and difference of opinion regarding

checked versus unchecked exceptions. With checked exceptions, a

function states explicitly exactly what kinds of exceptions it can

throw. With unchecked exceptions, it simply says "I can throw some

kind of exception." Java is probably the most famous example of a

checked exception system, with many other languages (including C#,

Python, and Ruby) having unchecked exceptions.

As usual, Haskell makes this interesting. Runtime exceptions are

most assuredly unchecked: all exceptions are converted to

SomeException via the Exception

typeclass, and function signatures do not state which specific

exception types can be thrown (for more on this, see next caveat).

Instead, this information is relegated to documentation, and

unfortunately is often not even covered there.

By contrast, approaches like ExceptT and EitherT are very explicit in the type of exceptions

that can be thrown. The cost of this is that there is extra

overhead necessary to work with functions that can return different

types of exceptions, usually by wrapping all possible exceptions in

a sum type.

This library isn't meant to settle the debate on checked vs

unchecked, but rather to bring sanity to Haskell's runtime

exception system. As such, this library is decidedly in the

unchecked exception camp, purely by virtue of the fact that the

underlying mechanism is as well.

Explicit vs implicit

Another advantage of the

ExceptT/EitherT approach is that you are

explicit in your function signature that a function may fail.

However, the reality of Haskell's standard libraries are that many,

if not the vast majority, of IO actions can throw some

kind of exception. In fact, once async exceptions are considered,

every IO action can throw an exception.

Once again, this library deals with the status quo of runtime

exceptions being ubiquitous, and gives the rule: you should

consider the IO type as meaning both that a function modifies the outside world, and may throw an exception (and, based on the previous caveat, may throw any type of exception it feels like).

There are attempts at alternative approaches here, such as

unexceptionalio.

Again, this library isn't making a value statement on one approach

versus another, but rather trying to make today's runtime

exceptions in Haskell better.

Type-based differentiation

As explained above, this library makes heavy usage of type

information to differentiate between sync and async exceptions.

While the approach used is fairly well respected in the Haskell

ecosystem today, it's certainly not universal, and definitely not

enforced by the Control.Exception module. In particular, throwIO will allow you to synchronously

throw an exception with an asynchronous type, and

throwTo will allow you to asynchronously throw an

exception with a synchronous type.

The functions in this library prevent that from happening via

exception type wrappers, but if an underlying library does

something surprising, the functions here may not work correctly.

Further, even when using this library, you may be surprised by the

fact that throw Foo `catch` (Foo -> ...) won't actually trigger the exception handler if Foo looks

like an asynchronous exception.

The ideal solution is to make a stronger distinction in the core

libraries themselves between sync and async exceptions.

Deadlock detection exceptions

Two exceptions types which are handled surprisingly are

BlockedIndefinitelyOnMVar and BlockedIndefinitelyOnSTM. Even though these exceptions

are thrown asynchronously by the runtime system, for our purposes

we treat them as synchronous. The reasons are twofold:

  • There is a specific action taken in the local thread - blocking

    on a variable which will never change - which causes the exception

    to be raised. This makes their behavior very similar to synchronous

    exceptions. In fact, one could argue that a function like

    takeMVar is synchronously throwing BlockedIndefinitelyOnMVar

  • By our standards of recoverable vs non-recoverable, these

    exceptions certainly fall into the recoverable category. Unlike an

    intentional kill signal from another thread or the user (via

    Ctrl-C), we would like to be able to detect that we entered a

    deadlock condition and do something intelligent in an

    application.

Possible future changes

Interruptible vs uninterruptible masking

This discussion is now being tracked at:

https://github.com/fpco/safe-exceptions/issues/3

In Control.Exception, allocation functions and cleanup handlers in combinators like bracket are masked using the (interruptible) mask function, in contrast to uninterruptibleMask. There have been some debates about the correctness of this in the past, notably a libraries mailing list discussion kicked off by Eyal Lotem.

It seems that general consensus is:

  • uninterruptibleMask is a better choice

  • But changing the core library like this would potentially break too many programs

In its current version, this library uses mask

(interruptible) for allocation functions and

uninterruptibleMask cleanup handlers. This is a

debatable decision (and one worth debating!). An example of

alternatives would be:

  • Use uninterruptibleMask for both allocation and cleanup pieces

  • Match Control.Exception's behavior

  • Provide two versions of each function, or possibly two modules

Naming of the synchronous monadic throwing function

We may decide to rename throw to something else at

some point. Please see

https://github.com/fpco/safe-exceptions/issues/4