Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Principles: Error handling #84

Closed
wants to merge 13 commits into from
225 changes: 225 additions & 0 deletions docs/project/principles/error_handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# Principles: Error handling

<!--
Part of the Carbon Language, under the Apache License v2.0 with LLVM
Exceptions. See /LICENSE for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-->

## Principles

### Programming errors are not recoverable

The Carbon language and standard library will not use recoverable
error-reporting mechanisms to report programming errors, i.e. errors that are
geoffromer marked this conversation as resolved.
Show resolved Hide resolved
caused by incorrect user code. Furthermore, Carbon's design will not prioritize
use cases involving recovery from programming errors.

Recovering from an error generally consists of discarding any state that might
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option is to roll back the damage to the state that was done by the error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better?

be invalidated by the original cause of the error, and then transferring control
to a point that doesn't depend on the discarded state. For example, a function
that reads data from a file and validates a checksum might avoid modifying any
nonlocal state until validation is successful, and return early if validation
fails. This recovery strategy relies on the fact that the likely causes of the
failure are known and bounded (probably a malformed input file or an I/O error),
which allows us to put a bound on the state that might have been invalidated.

However, when a programming error is detected, the original cause is neither
known nor bounded. For example, if a function dereferences a dangling pointer,
that might mean that the author of the function forgot to check some condition
before dereferencing, or that the caller incorrectly passed a dangling pointer,
or that some other code released the memory too early, among many other
possibilities. Consequently, the only way to (mostly) reliably recover from a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, please consider style when you're using parentheses. While I know you lean towards more use, I think things like this "(mostly)" could be better handled with a little rephrasing, like "the most effective way" or ".
https://developers.google.com/style/parentheses

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repeated ", or" plus other commas (", if", ", that", ", among") in this sentence makes it hard to read. Consider rewording, e.g.:

For example, ... might mean:

  • The author ...
  • The caller ...
  • Some other code ...
  • Or some other possibility.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Markdown considers bulleted lists to be separate paragraphs (with vertical whitespace above and below), so I'd rather not go that route. How's this?

programming error is to discard the entire address space and terminate the
program.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the conclusion, but I don't agree with the reasoning :) I think the reasoning is important, because it determines what is considered a programming error.

Consider an issue that is typically considered a recoverable error -- an operation that opens a file for reading by name determines that there's no such file. "When such an error is detected, the original cause is neither known nor bounded." It can have many causes: the programmer forgot to check whether the file exists, the programmer forgot to call the routine that creates the file, the programmer forgot to handle "out of disk space" error from the routine that creates the file, the programmer that wrote the script that invokes this program creates the file in the wrong directory, etc. "Without more information, it's impossible to know, so the only way to somewhat reliably recover from a programming error is to discard the entire address space and terminate the program."

Given this explanation, is there a distinction between dereferencing a dandling pointer and file not found error?

If you ask me, I'd explain it in terms of preconditions of APIs and language features. Violating a precondition is a programming error that is non-recoverable. If an API or a programming language feature has a requirement that some condition must hold, but it can detect a violation and return control to the caller, then it is not a programming error -- it is regular control flow (which may be expressed using error handling language features if we so desire).

A precondition can be something an API requires (for example, a file must exist, input array must be non-empty, input array must be sorted etc.), or the programming language requires (a pointer to be dereferenced must point to valid memory, addition should not overflow etc.)

"File not found" is usually not a programming error, but it is entirely reasonable to design an API where "file not found" is a non-recoverable error that terminates the program (think of a map reduce batch job). In that case, file being present and readable is a precondition, and violating it is a programming error. So what is a precondition and what is an error that can be handled really depends on the designer of the API or of the language feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this explanation, is there a distinction between dereferencing a dandling pointer and file not found error?

The distinction I would make is that in the case of a file-not-found error, we may not know the cause for certain, but the program can know (at least roughly) what the likely causes are. In the case of a dangling pointer, on the other hand, the program generally can't even know that, at least not with enough specificity to plausibly recover. I've tried to rephrase to make that clearer; does that help?

If you ask me, I'd explain it in terms of preconditions of APIs and language features. Violating a precondition is a programming error that is non-recoverable.

I agree with that, but it seems to just assert the position that I'm trying to justify here: that programming errors should be considered non-recoverable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The distinction I would make is that in the case of a file-not-found error, we may not know the cause for certain, but the program can know (at least roughly) what the likely causes are. In the case of a dangling pointer, on the other hand, the program generally can't even know that, at least not with enough specificity to plausibly recover. I've tried to rephrase to make that clearer; does that help?

I still don't see much of a distinction. It is often possible to make an informed guess about why the pointer is dangling -- think about all those times when a report from ASan that such and such pointer is used after free is all one needs to implement a fix even when one can't reproduce the problem locally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but the code handling the error, and the programmer writing that code, doesn't have access to that ASan report. Or if they do, they should probably just fix the bug, rather than try to detect and programmatically recover from it. I've tried to make this point more explicit; does that help?


Thus, we expect that supporting recovery from programming errors would provide
little or no benefit. Furthermore, it would be harmful to several of Carbon's
primary goals:

- [Performance-critical software](https://github.com/jonmeow/carbon-lang/blob/proposal-goals/docs/project/goals.md#performance-critical-software):
It would impose a pervasive performance overhead, because recoverable error
handling is never free, and a programming error can occur anywhere.
- [Code that is easy to read, understand, and write](https://github.com/jonmeow/carbon-lang/blob/proposal-goals/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write):
Because potential programming errors are pervasive, they would have to
propagate invisibly, which makes code harder to understand (see
geoffromer marked this conversation as resolved.
Show resolved Hide resolved
[below](#recoverable-errors-are-explicit-at-the-callsite)).
- [Software and language evolution](https://github.com/jonmeow/carbon-lang/blob/proposal-goals/docs/project/goals.md#both-software-and-language-evolution):
It would inhibit evolution of Carbon libraries, and the Carbon language, by
preventing them from changing how they respond to incorrect code.
- [Practical safety guarantees and testing mechanisms](https://github.com/jonmeow/carbon-lang/blob/proposal-goals/docs/project/goals.md#practical-safety-guarantees-and-testing-mechanisms):
Similarly, it would prevent Carbon users from choosing different
performance/safety tradeoffs for handling programming errors: if an
out-of-bounds array access is required to throw an exception, users can't
disable bounds checks, regardless of their risk tolerance, because code might
rely on those exceptions being thrown.

#### Examples

If Carbon supports assertions and/or contract checking, failed assertions will
not throw exceptions, even as an optional build mode. Assertion failures will
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like "throw exceptions" suddenly pulls a ton of context from C++ into this document...

Can this be phrased more generically?

Possible approach:

Suggested change
not throw exceptions, even as an optional build mode. Assertion failures will
not allow callers to detect and handle them, perhaps through a mechanism similar to C++ exceptions, even as an optional build mode. Assertion failures will

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better?

only be presented in ways that don't alter the program state, such as logging,
terminating the program, or trapping into a debugger.

### Memory exhaustion is not recoverable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the caveat you give below (or maybe instead? see my comments below) I think it would be good to pretty clearly call out that the goal is to address the underlying requirements here, just in a different way.

Basically, I think we don't want people to take away from this that Carbon won't be applicable in a sharply memory constrained environment. I think we're pretty committed to having some way to support such uses of Carbon if we want this to be viable in a wide range of environments. Just that the approach isn't expected to be for the default heap allocation mechanism to allow for recoverable failure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tricky, since we're talking about the standard library here. Is the goal to make sure that we don't prevent those users from using Carbon, or is it to make sure that those use cases get first-class support in the standard library? The former would just mean we need to make sure the language doesn't get in their way, which I think we definitely do want, but the latter would mean we have to provide alternative memory-exhaustion-compatible APIs for everything in the standard library, which I think we definitely don't want. At the level of a principles doc, I don't know how to spell out where between those extremes we intend Carbon to land.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate comment -- maybe worth clarifying that this is true for the default memory allocation APIs, but not necessarily all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better?


The Carbon standard library will not ordinarily use recoverable error-reporting
mechanisms to report memory exhaustion, or support user-defined code that does.

Memory exhaustion is not a programming error, and it is feasible to write code
that can successfully recover from it. However, the available evidence indicates
that very little C++ code actually does so correctly (see e.g. section 4.3 of
[this paper](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0709r4.pdf)),
which suggests that very little C++ code actually needs to do so, and we see no
reason to expect Carbon's users to differ in this respect.

Supporting recovery from memory exhaustion would impose many of the same harms
as supporting recovery from programming errors, and for the same basic reason:
memory allocation is pervasive, and so a mechanism for recovering from it would
have to be similarly pervasive. Furthermore, experience with C++ has shown that
attempting to support memory exhaustion can seriously deform the design of an
API.

#### Examples

The `pop` operation on a Carbon queue will return the value removed from the
queue. This is in contrast to C++'s `std::queue::pop()`, which does not return
the value popped from the queue, because
geoffromer marked this conversation as resolved.
Show resolved Hide resolved
[that would not be exception-safe](https://isocpp.org/blog/2016/06/quick-q-why-doesnt-stdqueuepop-return-value)
due to the possibility of an out-of-memory error while copying that value.
Instead, the user must first examine the front of the queue, and then pop it as
a separate operation. Not only is this awkward for users, it means that
concurrent queues cannot match the API of non-concurrent queues (because
separate `front()` and `pop()` calls would create a race condition).

#### Caveats

Carbon will probably provide a low-level way to allocate heap memory that makes
geoffromer marked this conversation as resolved.
Show resolved Hide resolved
allocation failure recoverable, because doing so appears to have few drawbacks.
However, users may need to build their own libraries on top of it, rather that
relying on the Carbon standard library, if they want to take advantage of it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like both of these statements are pushing a bit far into details and specifics that haven't materialized yet. I think they're more intended to be examples, but as written feel a bit sweeping in scope.

For example, I think we might work to enable parts of the standrad library to take advantage of different allocation strategies like this if we can find a clean way to incorporate it into the design. But it is a big "if", and I'm totally down with not overpromising. I just don't want to discourage too sharply either or preclude still open design exploration.

As I mentioned above, maybe we can replace specific caveats with a more general statement around working to explore and find ways of addressing the fundamental requirements of constrained systems programming which don't have as dramatic of an effect on the overall language and API design.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Working to explore" those use cases is pretty different from having them be an explicit goal (which you seem to be suggesting above), so I'm not sure what you're looking for here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "may need to build their own libraries on top of it" covers this adequately: it does leave open the possibility of a standard library that includes recovery from memory allocation failure.

I do want to avoid over-promising in the other case too, though: saying we may provide heap allocation that allows recovery from allocation failure rather than saying we definitely will.

There probably will not be a way to recover from _stack_ exhaustion, because
there is no known way of doing that without major drawbacks, and users who can't
tolerate crashing due to stack overflow can normally prevent it via static
geoffromer marked this conversation as resolved.
Show resolved Hide resolved
analysis.
josh11b marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little worried the second half here will be read to indicate that genuinely huge stack sizes will be necessary much like they are in C++.

I think we should (similar to above) actually address the use case for sharply limited stack size, but in a way that doesn't require recovering from arbitrary stack exhaustion.

As a concrete thing, I'd really love if we could allow threads to have very small data stacks by default while allowing them to grow cleanly to quite large when necessary. This would help reduce the address space pressure and other challenges of the current C++ model.

Anyways, mostly I worry we're getting too far into exactly how we will do this in Carbon rather than just the high level principle that the default memory allocation approach won't have recoverable errors on exhaustion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little worried the second half here will be read to indicate that genuinely huge stack sizes will be necessary much like they are in C++.

Can you say more? The connection between the two isn't obvious to me, because I don't see how recoverable stack-overflow errors can be used to mitigate a limited stack size, except in the very limited sense that they might let you isolate stack-overflow failures to a single computation, rather than the whole process. In other words, it seems like your system has to be designed so that the computations that you need to actually work will fit within the stack size limit, regardless of whether stack overflows are recoverable.

Anyways, mostly I worry we're getting too far into exactly how we will do this in Carbon rather than just the high level principle that the default memory allocation approach won't have recoverable errors on exhaustion.

At least in the case of stack exhaustion, isn't that pretty much what I've done?


### Recoverable errors are explicit in function declarations

Carbon functions that can emit recoverable errors will always be explicitly
marked in all function declarations.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to explicitly call out that this may simply be via the return type?

Otherwise, I can see this reading as an explicit marking beyond the return type even if we end up completely representing the error in the return type...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better?


The possibility of emitting recoverable errors is nearly as fundamental to a
function's API as its return type, and so Carbon APIs will be substantially
clearer to read, and safer to use, if we require consistent, compiler-checked
documentation of that property. Furthermore, as noted above, the mechanisms for
emitting a recoverable error always impose some performance overhead, so the
compiler must be able to distinguish the functions that need that overhead from
the ones that do not.

The default should be that functions do not emit errors, because that's the
simpler and more efficient behavior, and we also expect it to be the common
case.

#### Caveats

This principle applies only to the mechanisms that Carbon natively provides for
error recovery. We cannot guarantee that errors are explicitly marked if they
are conveyed via some other mechanism (e.g. `errno`-style thread-local storage).
geoffromer marked this conversation as resolved.
Show resolved Hide resolved

### Recoverable errors are explicit at the callsite

Operations that can emit recoverable errors will always be explicitly marked at
the point of use.

If errors can propagate silently (as with exceptions in most languages), it
creates control flow paths that are not visible to the reader of the code, and
it is extremely difficult to reason about procedural code when you aren't aware
of all control flow paths. This would make Carbon code harder to understand,
maintain, and debug.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced on this paragraph. My canonical example for when the "invisible control flow" has been helpful for me in the past is writing network code. I can't always verify the data is good before doing any work (doing so might imply doing double the work or waiting to begin processing one packet until the next packet has arrived), so an error can occur fairly deep in the call stack. For certain types of errors, bad data suggests some sort of corruption somewhere. There is frequently no sane way to recover, so the program logic I want to express is "Go back to the code where I created this socket, clean up anything that I created since then, disconnect, and reconnect". Exceptions handle that perfectly and localize the error to the two places that care about it.

You somewhat address this later on in the section "Error propagation must be straightforward". To be convinced of this, I would want to see an alternative strategy that is at least somewhat comparable. I don't expect it to be as nice as "no code", but I want to understand just how much of that I am giving up to get the benefits you describe before I could say I support this paragraph.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rust and Swift mark the propagation of an error with a single token at the callsite (postfix ? and prefix try, respectively), which seems about as close to "no code" as you can get while still being code (admittedly, in some cases you may also need parentheses for disambiguation, as with any other unary operator). That's the kind of thing I have in mind when I say propagation should be straightforward.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What might help strengthen the argument is to talk about the experience of a reader in the middle of the propagation, who is less familiar with the code than the author. This is IMO where the readability hit is felt most -- otherwise as David says it can feel like an effective way to separate concerns. But a reader who is trying to understand the behavior of code in the middle and is unaware that control flow doesn't proceed as expected based on the locally visible code can be left completely lost and having to read a much larger amount of code both up and down the call stack to understand what the local behavior is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better?


Conversely, if errors can be silently ignored (as with error return codes in
many languages), it creates a major risk of accidentally resuming normal
execution without actually recovering from the error (i.e. discarding
invalidated state). This, too, would make it extremely difficult to reason
correctly about Carbon code.

Either possibility would also allow code to evolve in unsafe ways. Changing a
function to allow it to emit errors is semantically a breaking change: client
code must now contend with a previously-impossible failure case. Requiring
geoffromer marked this conversation as resolved.
Show resolved Hide resolved
errors to be marked at the callsite ensures that this breakage manifests at
build time.

josh11b marked this conversation as resolved.
Show resolved Hide resolved
#### Caveats

We cannot prevent errors from being ignored if they are conveyed via a
nonstandard mechanism (e.g. `errno`).
geoffromer marked this conversation as resolved.
Show resolved Hide resolved

### Error propagation must be straightforward

Carbon will provide a means to propagate recoverable errors from any function
call to the caller of the enclosing function, with minimal textual overhead.

In our experience, it is very common for C++ code to propagate errors across
multiple layers of the call stack. C++ exceptions support this natively, and
programmers in environments without exceptions usually develop a lightweight way
to propagate errors explicitly, typically via a macro containing a conditional
`return`. In some cases they even resort to using nonstandard language
extensions in order to be able to use this operation within expressions, rather
than only at the statement level.

Given the ubiquity of this use case, Carbon must provide support for it that can
be used without altering the structure of the code, or making the non-error-case
logic less clear.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you con make this a bit more squishy without removing the importance of it:

Suggested change
Given the ubiquity of this use case, Carbon must provide support for it that can
be used without altering the structure of the code, or making the non-error-case
logic less clear.
Given the ubiquity of this use case, Carbon must provide support for it that can
be used with minimal changes to the structure of the code, or making the non-error-case
logic less clear.

I think this also avoids a debate over "is it really altering the structure?" by instead focusing on how much structural churn is necessary.


### No universal error categories

Carbon will not establish an error hierarchy or other reusable error vocabulary,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this conflates two questions.
(1) Does Carbon itself, either in the core language or in the standard library, establish an error hierarchy?
(2) Does Carbon allow/encourage/require users to define their own hierarchy?

The text itself mainly answers question 1, but the argument about brittle code also applies to question 2. I believe it's important to address both.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this conflates those questions, but I think that conflation is correct: the two questions are aspects of one underlying question, namely whether classifying propagated errors is a programming practice that Carbon will encourage. I've tweaked part of the next paragraph to be less specific to (1); are there other places that you think put too much emphasis on (1), or not enough emphasis on (2)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"or other reusable error vocabulary" seems a bit over-broad to me. Go's error interface seems pretty harmless to me (in particular it doesn't require so many type shenanigans as Rust's Error trait), and your arguments about the downside of hierarchy and classification don't seem to apply to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I phrased this poorly; I didn't intend to exclude things like that. Better?

and will not prioritize use cases that involve branching based on the properties
of a propagated error.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second part of this doesn't really make sense to me when first reading it. Reading the rest, I think I understand, but maybe to help clarify:

Suggested change
Carbon will not establish an error hierarchy or other reusable error vocabulary,
and will not prioritize use cases that involve branching based on the properties
of a propagated error.
Carbon will not establish an error hierarchy or other reusable error vocabulary,
and will not prioritize use cases that involve classifying and reacting to any
common set of properties of a propagated error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, except that I've omitted "common set" because I actually mean this to cover any properties.


Some languages attempt to impose a hierarchy or some other global classification
scheme for errors, in order to allow code to respond differently to different
kinds of errors, even after the errors have propagated some distance from the
function that originally raised them. However, this practice tends to be quite
brittle, because it almost inevitably requires relying on implementation
details: if a function's contract gives different meanings to different errors
it emits, it generally can't satisfy that contract by blindly propagating errors
from the functions it calls. Conversely, if it doesn't have such a contract, its
callers normally can't differentiate among the errors it emits without depending
on its implementation details.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like an argument against including an easy to use construct to propagate errors, rather than an argument against universal error classification APIs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it's an argument against having both convenient error propagation and universal error classification. But as argued above, I think we need convenient error propagation, so classification has to be what we drop.


It may make sense to distinguish certain categories of errors, if any layer of
the stack can in principle respond to those errors, and the appropriate response
requires only local knowledge. For example, any layer of the stack can respond
to an out-of-memory error by e.g. releasing any unused caches. Similarly, any
layer of the stack can respond to thread cancellation by ceasing any new
computational work and propagating the signal _even if_ it could otherwise
continue despite a failiure at that point.
geoffromer marked this conversation as resolved.
Show resolved Hide resolved

However, such cases are caught between the horns of a dilemma: any error that's
universal enough to be meaningful across arbitrary levels of the call stack is
likely to be too pervasive for explicitly-marked propagation to be tolerable.
Both of the above examples have that problem; we've already ruled out
propagating out-of-memory errors because of their pervasiveness, and
cancellation is likely to pose similar challenges (although cancellation can be
ignored, which may simplify the problem somewhat).

It is certainly possible to structure a codebase so that you can reliably
propagate errors across multiple layers of the stack (so long as you control
those layers), and Carbon will support those use cases. However, it will do so
as a byproduct of general-purpose programming facilities such as pattern
matching; Carbon will not provide a separate sugar syntax for pattern-matching
error metadata, especially if that syntax can encompass multiple
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this is actually an independent principle that would be worth having: the desire to minimize (and potentially avoid) having fundamental language constructs or control flow constructs whose only purpose is error handling, and instead to try to ensure the general facilities of the language are sufficient. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed offline, I regard that as a nice-to-have rather than a requirement, and I can easily imagine wanting to trade it off for priorities like readability, so I'm hesitant to enshrine it as a principle.

potentially-failing operations. For example, if Carbon supports `try`/`catch`
statements, they will always have a single `catch` block, which will be invoked
for any error that escapes the `try` block.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example loses me, i think because it is imagining a fairly specific thing and I just don't have the context.

How essential is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's important to provide a concrete example, because the previous discussion has been pretty abstract. However, if it's losing you, it's not doing that job. Let me try to elaborate, and then hopefully you can suggest how to phrase it more clearly without spending a paragraph on it:

There's a very common language feature that lets you specify a block of code and a set of pattern/handler pairs, and if an exception escapes the block, control is transferred to the handler whose pattern best matches the exception. try/catch in C++, Java, and JavaScript, try/except in Python, and do/catch in Swift are all examples. However, this feature really combines two separate pieces of functionality:

  1. Defining a scope at which exception propagation stops, and control is transferred back to user code
  2. Branching to one of several blocks of code based on the pattern that the exception matches

The primary practical consequence of the passage above is that Carbon will not have #2, but I don't want to just say "Carbon won't have try/catch", because nothing we've said so far has ruled out having #1 on its own, i.e. having a form of try/catch that doesn't incorporate pattern matching.


## Other resources

Several other groups of language designers have arrived at similar principles.
See e.g. Swift's
[error handling rationale](https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst),
[Joe Duffy's account](http://joeduffyblog.com/2016/02/07/the-error-model) of
Midori's error model, and Herb Sutter's
[pending proposal](http://wg21.link/P0709) for a new approach to exceptions in
C++.