blob: 37cef42143cbf7e5579a351128c7e59b4e596b8f [file] [log] [blame] [view]
# Error Handling — Problem Overview
Russ Cox\
August 27, 2018
## Introduction
This overview and the accompanying
[detailed draft design](go2draft-error-handling.md)
are part of a collection of [Go 2 draft design documents](go2draft.md).
The overall goal of the Go 2 effort is to address
the most significant ways that Go fails to scale
to large code bases and large developer efforts.
One way that Go programs fail to scale well is in the
writing of error-checking and error-handling code.
In general Go programs have too much code checking errors
and not enough code handling them.
(This will be illustrated below.)
The draft design aims to address this problem by introducing
lighter-weight syntax for error checks
than the current idiomatic assignment-and-if-statement combination.
As part of Go 2, we are also considering, as a separate concern,
changes to the [semantics of error values](go2draft-error-values-overview.md),
but this document is only about error checking and handling.
## Problem
To scale to large code bases, Go programs must be lightweight,
[without undue repetition](https://www.youtube.com/watch?v=5kj5ApnhPAE),
and also robust,
[dealing gracefully with errors](https://www.youtube.com/watch?v=lsBF58Q-DnY)
when they do arise.
In the design of Go, we made a conscious choice
to use explicit error results and explicit error checks.
In contrast, C most typically uses explicit checking
of an implicit error result, [errno](http://man7.org/linux/man-pages/man3/errno.3.html),
while exception handlingfound in many languages,
including C++, C#, Java, and Python—represents implicit checking of implicit results.
The subtleties of implicit checking are covered well in
Raymond Chens pair of blog posts,
"[Cleaner, more elegant, and wrong](https://devblogs.microsoft.com/oldnewthing/20040422-00/?p=39683)" (2004),
and "[Cleaner, more elegant, and harder to recognize](https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36693)" (2005).
In essence, because you cant see implicit checks at all,
it is very hard to verify by inspection that the error handling code
correctly recovers from the state of the program at the time the check fails.
For example, consider this code, written in a hypothetical dialect of Go with exceptions:
func CopyFile(src, dst string) throws error {
r := os.Open(src)
defer r.Close()
w := os.Create(dst)
io.Copy(w, r)
w.Close()
}
It is nice, clean, elegant code.
It is also invisibly wrong: if `io.Copy` or `w.Close` fails,
the code does not remove the partially-written `dst` file.
On the other hand, the equivalent actual Go code today would be:
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return err
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return err
}
defer w.Close()
if _, err := io.Copy(w, r); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
}
This code is not nice, not clean, not elegant, and still wrong:
like the previous version, it does not remove `dst` when `io.Copy` or `w.Close` fails.
There is a plausible argument that at least a visible check
could prompt an attentive reader to wonder about
the appropriate error-handling response at that point in the code.
In practice, however, error checks take up so much space
that readers quickly learn to skip them to see the structure of the code.
This code also has a second omission in its error handling.
Functions should typically [include relevant information](https://golang.org/doc/effective_go.html#errors)
about their arguments in their errors,
like `os.Open` returning the name of the file being opened.
Returning the error unmodified produces a failure
without any information about the sequence of operations that led to the error.
In short, this Go code has too much error checking
and not enough error handling.
A more robust version with more helpful errors would be:
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if _, err := io.Copy(w, r); err != nil {
w.Close()
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if err := w.Close(); err != nil {
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}
Correcting these faults has only made the code more correct, not cleaner or more elegant.
## Goals
For Go 2, we would like to make error checks more lightweight,
reducing the amount of Go program text dedicated to error checking.
We also want to make it more convenient to write error handling,
raising the likelihood that programmers will take the time to do it.
Both error checks and error handling must remain explicit,
meaning visible in the program text.
We do not want to repeat the pitfalls of exception handling.
Existing code must keep working and remain as valid as it is today.
Any changes must interoperate with existing code.
As mentioned above, it is not a goal of this draft design
to change or augment the semantics of errors.
For that discussion see the [error values problem overview](go2draft-error-values-overview.md).
## Draft Design
This section quickly summarizes the draft design,
as a basis for high-level discussion and comparison with other approaches.
The draft design introduces two new syntactic forms.
First, it introduces a checked expression `check f(x, y, z)` or `check err`,
marking an explicit error check.
Second, it introduces a `handle` statement defining an error handler.
When an error check fails, it transfers control to the innermost handler,
which transfers control to the next handler above it,
and so on, until a handler executes a `return` statement.
For example, the corrected code above shortens to:
func CopyFile(src, dst string) error {
handle err {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
handle err {
w.Close()
os.Remove(dst) // (only if a check fails)
}
check io.Copy(w, r)
check w.Close()
return nil
}
The `check`/`handle` combination is permitted in functions
that do not themselves return errors.
For example, here is a main function from a
[useful but trivial program](https://github.com/rsc/tmp/blob/master/unhex/main.go):
func main() {
hex, err := ioutil.ReadAll(os.Stdin)
if err != nil {
log.Fatal(err)
}
data, err := parseHexdump(string(hex))
if err != nil {
log.Fatal(err)
}
os.Stdout.Write(data)
}
It would be shorter and clearer to write instead:
func main() {
handle err {
log.Fatal(err)
}
hex := check ioutil.ReadAll(os.Stdin)
data := check parseHexdump(string(hex))
os.Stdout.Write(data)
}
For details, see the [draft design](go2draft-error-handling.md).
## Discussion and Open Questions
These draft designs are meant only as a starting point for community discussion.
We fully expect the details to be revised based on feedback and especially experience reports.
This section outlines some of the questions that remain to be answered.
**Check versus try**.
The keyword `check` is a clear statement of what is being done.
Originally we used the well-known exception keyword `try`.
This did read well for function calls:
data := try parseHexdump(string(hex))
But it did not read well for checks applied to error values:
data, err := parseHexdump(string(hex))
if err == ErrBadHex {
... special handling ...
}
try err
In this case, `check err` is a clearer description than `try err`.
Rust originally used `try!` to mark an explicit error check
but moved to a special `?` operator instead.
Swift also uses `try` to mark an explicit error check,
but also `try!` and `try?`, and as part of a broader
analogy to exception-handling that also includes `throw` and `catch`.
Overall it seems that the draft designs `check`/`handle`
are sufficiently different from exception handling
and from Rust and Swift to justify the clearer keyword,
`check`, over the more familiar one, `try`.
Both Rust and Swift are discussed in more detail below.
**Defer**.
The error handling is in some ways similar to [`defer`](https://golang.org/ref/spec#Defer_statements) and
[`recover`](https://golang.org/ref/spec#Handling_panics),
but for errors instead of panics.
The current draft design makes error handlers chain lexically,
while `defer` builds up a chain at runtime
depending on what code executes.
This difference matters for handlers (or deferred functions)
declared in conditional bodies and loops.
Although lexical stacking of error handlers seems like a marginally better design,
it may be less surprising to match `defer` exactly.
As an example where `defer`-like handling would be more convenient,
if `CopyFile` established its destination `w` as either `os.Stdout` or the result of `os.Create`,
then it would be helpful to be able to introduce the `os.Remove(dst)` handler conditionally.
**Panics**.
Weve spent a while trying to harmonize error handling and panics,
so that cleanup due to error handling need not be repeated for cleanup due to panics.
All our attempts at unifying the two only led to more complexity.
**Feedback**.
The most useful general feedback would be examples of interesting uses
that are enabled or disallowed by the draft design.
Wed also welcome feedback about the points above,
especially based on experience with complex
or buggy error handling in real programs.
We are collecting links to feedback at
[golang.org/wiki/Go2ErrorHandlingFeedback](https://golang.org/wiki/Go2ErrorHandlingFeedback).
## Designs in Other Languages
The problem section above briefly discussed C and exception-based languages.
Other recent language designs have also recognized
the problems caused by exception handlings invisible error checks,
and those designs are worth examining in more detail.
The Go draft design was inspired, at least in part, by each of them.
## Rust
Like Go, [Rust distinguishes](https://doc.rust-lang.org/book/second-edition/ch09-00-error-handling.html)
between expected errors, like "file not found", and unexpected errors,
like accessing past the end of an array.
Expected errors are returned explicitly
while unexpected errors become program-ending panics.
But Rust has little special-purpose
language support for expected errors.
Instead, concise handling of expected errors
is done almost entirely by generics.
In Rust, functions return single values (possibly a single tuple value),
and a function returning a potential error returns a
[discriminated union `Result<T, E>`](https://doc.rust-lang.org/book/second-edition/ch09-02-recoverable-errors-with-result.html)
that is either the successful result of type `T` or an error of type `E`.
enum Result<T, E> {
Ok(T),
Err(E),
}
For example, `fs::File::Open` returns a `Result<fs::File, io::Error>`.
The generic `Result<T, E>` type defines an
[unwrap method](https://doc.rust-lang.org/book/second-edition/ch09-02-recoverable-errors-with-result.html#shortcuts-for-panic-on-error-unwrap-and-expect)
that turns a result into the underlying value (of type `T`)
or else panics (if the result represents an error).
If code does want to check an error instead of panicking,
[the `?` operator](https://doc.rust-lang.org/book/second-edition/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator)
macro-expands `use(result?)` into the Rust equivalent of this Go code:
if result.err != nil {
return result.err
}
use(result.value)
The `?` operator therefore helps shorten the error checking
and is very similar to the draft designs `check`.
But Rust has no equivalent of `handle`:
the convenience of the `?` operator comes with
the likely omission of proper handling.
Rusts equivalent of Gos explicit error check `if err != nil` is
[using a `match` statement](https://doc.rust-lang.org/book/second-edition/ch09-02-recoverable-errors-with-result.html),
which is equally verbose.
Rusts `?` operator began life as
[the `try!` macro](https://doc.rust-lang.org/beta/book/first-edition/error-handling.html#the-real-try-macro).
## Swift
Swifts `try`, `catch`, and `throw` keywords appear at first glance to be
implementing exception handling, but really they are syntax for explicit error handling.
Each functions signature specifies whether the function
can result in ("throw") an error.
Here is an [example from the Swift book](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html#ID510):
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
These are analogous to the Go result lists `(string, error)` and `string`.
Inside a "throws" function, the
`throw` statement returns an error,
as in this [example, again from the Swift book](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html#ID509):
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
Every call to a "throws" function must specify at the call site
what to do in case of error. In general that means nesting the call
(perhaps along with other calls) inside a [do-catch block](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html#ID541),
with all potentially-throwing calls marked by the `try` keyword:
do {
let s = try canThrowErrors()
let t = cannotThrowErrors()
let u = try canThrowErrors() // a second call
} catch {
handle error from try above
}
The key differences from exception handling as in C++, Java, Python,
and similar languages are:
- Every error check is marked.
- There must be a `catch` or other direction about what to do with an error.
- There is no implicit stack unwinding.
Combined, those differences make all error checking,
handling, and control flow transfers explicit, as in Go.
Swift introduces three shorthands to avoid having
to wrap every throwing function call in a `do`-`catch` block.
First, outside a block, `try canThrowErrors()`
[checks for the error and re-throws it](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html#ID510),
like Rusts old `try!` macro and current `?` operator.
Second, `try! canThrowErrors()`
[checks for the error and turns it into a runtime assertion failure](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html#ID513),
like Rusts `.unwrap` method.
Third, `try? canThrowErrors()`
[evaluates to nil on error, or else the functions result](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html#ID542).
The Swift book gives this example:
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
The example discards the exact reasons these functions failed.
For cleanup, Swift adds lexical [`defer` blocks](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html#ID514),
which run when the enclosing scope is exited, whether by an explicit `return` or by throwing an error.