Jon Pretty,

A Manifesto for Error Management

IN THE FIRST POST of this series I set the scene for error handling in Scala. Our desire is to develop reliable programs; software which does not crash unexpectedly; applications which give their users accurate, precise and informative feedback in exceptional circumstances.

But we want to achieve that goal with expressive code; following a continuum from a working prototype to production durability; yet with the plasticity to adapt to change throughout development and maintenance, without compromising safety.

I have attempted to encode these desires in a draft manifesto. This is a statement of aspiration for the framework I want to write code in when I’m developing an application, whether that be a command-line tool, a Scala.JS script running in a browser, or a server responding to HTTP requests. But this manifesto is made with the qualified hindsight that it’s not just aspirational. It’s achievable.

The Error Management Manifesto

The manifesto is expressed as twelve principles. I have written it with Scala in mind, because that is my domain of expertise. But its concepts are more widely applicable. However, the nature of the requirements means that many programming languages—notably dynamically-typed languages—are quickly excluded: they are foundationally incompatible. While for others we can remain hopeful.

There is one brief note on terminology: we will use the verb raises in prose, and later in code, to describe the possibility that an expression will encounter a certain type of error when it is executed and needs to handle it.

While any partial expression may encounter an error on some occasions and return a successful value on others, we might be tempted to indicate this uncertainty by saying that the expression may raise an error or could raise an error.

But we consider may, could, can and might to be superfluous: an expression which may raise an error surely does on some occasions. And so we simply say it raises an error, and understand implicitly that the error is a possibility rather than a certainty.

The Twelve Principles

I. Typed Values

Errors are represented as values, distinguishable from each other at compiletime by their type and further at runtime by their field values, which capture certain state relevant to understanding the error.

II. Static Analysis

Code is analysed during compilation to statically determine which error types may arise within every expression or block, and expressed between the programmer and the compiler through code and error messages.

III. Functional Totality

The compiler exhaustively guarantees that every error type raised during the evaluation of an expression or block is handled: eliminated, transformed into a different error type, or delegated to another handler.

IV. Suppressible Enforcement

Enforcement of error handling may be explicitly suppressed by type, allowing compilation of expressions whose error cases are not handled, or which are only partially handled.

V. Incremental Enhancement

Code can evolve gradually towards production-readiness as error-handling code is added, incrementally and continuously, without any need to rewrite significant amounts of code at any point along that continuum.

VI. Transparent Composition

The addition of error handling has no effect on the ability to compose expressions verbatim. In particular, the principal type of each expression is unaffected by error handling, though additional information may be conveyed in an expression’s type.

VII. Scoped Granularity

Error handling is applicable to structural regions of code varying in granularity from simple localized expressions to large blocks, delimited as lexical scopes.

VIII. Analytic Obviation

Error handling may be obviated when static analysis determines that an error of some type can not be raised within an expression or block.

IX. Contextual Transmutation

Errors of some type may be routinely transformed into a more meaningful error of a different type in a different context. Several heterogeneous error types may be unified into a single homogeneous error type.

X. Delimited Accrual

Errors do not necessarily terminate execution, and contexts may be delimited to let subsequent errors be aggregated with the first, without affecting the ultimate failure of the context.

XI. Imperishable Concurrency

Errors that occur in concurrent contexts are always handled and processed, in the thread where they arise or elsewhere; a thread should not fail silently without its error being handled somewhere.

XII. Configurable Diagnostics

The diagnostic information associated with an error, such as its stack trace, is configurable and may be supplemented with contextual details. In particular, expensive diagnostic details may be turned off for performance.

The Manifesto and Scala

These principles describe the programming environment—the language features and library support—that I believe is necessary to follow a smooth development process that leads to correct, maintainable software.

And the implementation is provided by Soundness and several features of Scala 3 which make it possible.

Scala 3’s static analysis (II) through its advanced typechecker is the necessary foundation of everything that follows, though Scala inherits its representation of errors as typed values (I) from Java. Scala’s contextual search system is heavily utilized to enforce functional totality (III) and, combined with dependent typing, is the means by which analytic obviation (VIII) can be achieved. The language’s strong focus on nested lexical scopes makes scoped granularity (VII) the only natural approach (but with important details enforced by capture checking), and enables incremental enhancement (V) and suppressible enforcement (IV). The relatively new style of writing Scala, called ”direct-style Scala”, is the philosophy by which context functions provide transparent composition (VI).

Contingency

The Contingency module of Soundness implements macros that provide contextual transmutation (IX) of errors and delimited accrual (X), as well as some facilities for enhanced, configurable diagnostics (XII). And Parasite implements structured concurrency with specific attention to error handling for imperishable concurrency (XI).

Scala 3 and Contingency is not necessarily a unique combination. Riccardo Cardin’s raise4s, which is still in its infancy, provides similar functionality to Contingency with different syntax, and later versions may go further. Other statically-typed languages like Haskell provide a suitable foundation to accommodating every principle.

Summary

Here, we have seen a set of principles that encodes the features of a desirable programming framework for writing safe code.

What this manifesto does not include (not directly, at least) is advice on how to get the most out of these features. As much as possible, the manifesto coerces developers to use good practices. But there remains some flexibility within its constraints; flexibility to make superior or inferior design choices.

That advice to encourage the superior choices will be shared throughout the remainder of this blog series. But in the next installment, I start exploring the real code that makes the manifesto a reality.