• Stars
    star
    132
  • Rank 274,205 (Top 6 %)
  • Language
    Haskell
  • License
    MIT License
  • Created over 8 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Safe, consistent, and easy exception handling

safe-exceptions

Safe, consistent, and easy exception handling

Tests Stackage

The documentation for this library is available on Stackage

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. It provides a safe and simple API on top of the existing exception handling machinery. The API is equivalent to the underlying implementation in terms of power but encourages best practices to minimize the chances of getting the exception handling wrong. By doing so 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.

NOTE The UnliftIO.Exception module in the unliftio library provides a very similar API to this module, but based around the MonadUnliftIO typeclass instead of MonadCatch and MonadMask. The unliftio release announcement explains why this may be considered preferable from a safety perspective.

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. You may also be interested in browsing through the cookbook. There is also an exception safety tutorial on haskell-lang.org which is based on this package.

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 exception system in GHC and state that any exception 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.

More Repositories

1

mezzohaskell

Community-driven book on intermediate Haskell
298
star
2

inline-c

Haskell
284
star
3

terraform-aws-foundation

Establish a solid Foundation on AWS with these modules for Terraform
HCL
204
star
4

unliftio

The MonadUnliftIO typeclass for unlifting monads to IO
Haskell
150
star
5

ide-backend

ide-backend drives the GHC API to build, query, and run your code
Haskell
120
star
6

minghc

DEPRECATED: Windows installer for GHC including msys
Haskell
108
star
7

typed-process

Alternative API for processes, featuring more type safety
Haskell
106
star
8

weigh

Measure allocations of a Haskell functions/values
Haskell
92
star
9

amber

Manage secret values in-repo via public key cryptography
Rust
83
star
10

applied-haskell

80
star
11

pid1

Do signal handling and orphan reaping for Unix PID1 init processes
Haskell
76
star
12

ghc-prof-flamegraph

Haskell
75
star
13

haskell-scratch

Base Docker image which includes minimal shared libraries for GHC-compiled executables
Makefile
67
star
14

http-reverse-proxy

Reverse proxy HTTP requests, either over raw sockets or with WAI
Haskell
54
star
15

rdr2tls

Haskell web service that redirects all traffic from HTTP to HTTPS
Haskell
52
star
16

ghcjs-react

React bindings for GHCJS
JavaScript
48
star
17

odbc

Haskell ODBC binding with SQL Server support
Haskell
44
star
18

streaming-commons

Common lower-level functions needed by various streaming data libraries
Haskell
36
star
19

schoolofhaskell

Haskell
34
star
20

cache-s3

Haskell
34
star
21

halogen-form

Formlets for halogen
PureScript
30
star
22

say

Send textual messages to a Handle in a thread-friendly way
Haskell
29
star
23

optparse-simple

Simple helper functions to work with optparse-applicative
Haskell
29
star
24

haskell-ide

Repo for collaborating on various shared Haskell IDE components.
Haskell
29
star
25

stackage-cli

Haskell
28
star
26

devops-helpers

Devops helper scripts
Shell
28
star
27

wai-middleware-auth

Authentication middleware that secures WAI application
Haskell
23
star
28

safe-decimal

Haskell
22
star
29

pid1-rs

pid1 handling library for proper signal and zombie reaping of the PID1 process
Rust
22
star
30

stackage-view

JavaScript
21
star
31

best-practices

Documentation for best practices followed at FP Complete
21
star
32

inline-c-cpp

Haskell
19
star
33

sift

Sift through Haskell code for analysis purposes
Haskell
18
star
34

schoolofhaskell.com

Haskell
16
star
35

haskell-multi-docker-example

Haskell
15
star
36

mutable-containers

Abstactions and concrete implementations of mutable containers
14
star
37

monad-unlift

Typeclasses for representing monad (transformer) morphisms
Haskell
14
star
38

stack-docker-image-build

Generate Docker images containing additional packages
Haskell
14
star
39

simple-file-mirror

A dumb tool to mirror changes in a directory between hosts
Haskell
12
star
40

inline-c-nag

Haskell
11
star
41

stream

Streaming data library built around first-class stream fusion for high efficiency
Haskell
11
star
42

yesod-ghcjs

Haskell
10
star
43

th-utilities

Collection of useful functions for use with Template Haskell
Haskell
10
star
44

stackage-curator

DEPRECATED Tools for curating Stackage package sets and building reusable package databases
Haskell
10
star
45

rust-aws-devops

A very small DevOps tool to demo using Rust with AWS
Rust
10
star
46

haskell-filesystem

Contains the system-filepath and system-fileio packages
Haskell
9
star
47

fpco-salt-formula

SaltStack
9
star
48

hackage-mirror

8
star
49

wai-middleware-consul

Haskell
8
star
50

stackage-update

Update your package index incrementally (requires git)
Haskell
8
star
51

fsnotify-conduit

Get filesystem notifications as a stream of events
Haskell
8
star
52

packer-windows

The example code repo to accompany the (yet to be released) blog post on building Windows server images using Packer
PowerShell
8
star
53

flush-queue

Haskell
8
star
54

conduit-combinators

Commonly used conduit functions, for both chunked and unchunked data
8
star
55

fuzzcheck

A library for testing monadic code in the spirit of QuickCheck
Haskell
8
star
56

monad-logger-syslog

monad-logger for syslog
Haskell
7
star
57

bootstrap-salt-formula

SaltStack
6
star
58

ghcjs-from-typescript

Haskell
6
star
59

serial-bench

Ridiculously oversimplified serialization benchmark.
HTML
5
star
60

docker-static-haskell

Docker images based on Alpine for compiling static Haskell executables
5
star
61

stackage-upload

A more secure version of cabal upload which uses HTTPS
Haskell
4
star
62

openconnect-gateway

Docker image and helper scripts to run a VPN gateway with OpenConnect
Shell
4
star
63

executable-hash

Provides the SHA1 hash of the program executable
Haskell
4
star
64

stackage-install

Secure download of packages for cabal-install
Haskell
4
star
65

simple-poll

Haskell
4
star
66

alpine-haskell-stack

Just
4
star
67

monad-logger-json

Functions for logging ToJSON instances with monad-logger
Haskell
4
star
68

demos

Haskell
3
star
69

ghc-rc-stackage

Helper repo for testing GHC release candidates against the Stackage package set
Shell
3
star
70

wai-middleware-ldap

Haskell
3
star
71

stackage-sandbox

Haskell
3
star
72

replace-process

Replace the current process with a different executable
Haskell
3
star
73

strict-concurrency

Mirror of strict-concurrency Darcs repository by Don Stewart
Haskell
3
star
74

kube-test-suite

Validation test suite for kubernetes clusters
Haskell
3
star
75

qapla

Haskell
2
star
76

spiderweb

Link check, capture, and serve sites statically
Haskell
2
star
77

fphc

Issue tracker for FP Haskell Center
2
star
78

wai-middleware-crowd

Middleware and utilities for using Atlassian Crowd authentication
Haskell
2
star
79

stackage-dot

Haskell
2
star
80

hauth

Header Authentication Library for Haskell
Haskell
2
star
81

stackage-setup

Haskell
2
star
82

libraries

FP Complete libraries mega-repo
Haskell
2
star
83

snoc-vector

Vectors with cheap append operations
Haskell
2
star
84

helm-charts

FPCO Helm charts
Smarty
2
star
85

ghc-events-time

Haskell
2
star
86

stackage-cabal

Haskell
2
star
87

chunked-data

Typeclasses for dealing with various chunked data representations
2
star
88

build-profile

Haskell
2
star
89

dockerfile-argocd-deploy

Dockerfile for image containing tools for CI jobs to deploy to Kubernetes using ArgoCD
Dockerfile
1
star
90

simple-mega-repo

Haskell
1
star
91

hackage-upload-analyzer

Performs batch analysis of Hackage upload logs, generating an HTML file with the results.
Haskell
1
star
92

text-stream-decode

Streaming decoding functions for UTF encodings.
Haskell
1
star
93

fpco-cereal

Haskell
1
star
94

nrelic

New Relic GCStats metrics (demo)
Haskell
1
star
95

piggies

Haskell
1
star
96

pretty-time

Rendering of time values in various user-friendly formats.
Haskell
1
star
97

soh-static

School of Haskell static archive
HTML
1
star
98

coredump-uploader

Shell
1
star
99

stack-templates

fpco stack templates
1
star
100

rest-client

Haskell
1
star