blob: b6e30165acbab827e4701e4c92a218e44e9e13e9 [file] [log] [blame] [view]
# Error Handling — Draft Design
Marcel van Lohuizen\
August 27, 2018
## Abstract
We present a draft design to extend the Go language with dedicated error handling constructs.
These constructs are in the spirit of "[errors are values](https://blog.golang.org/errors-are-values)"
but aim to reduce the verbosity of handling errors.
For more context, see the [error handling problem overview](go2draft-error-handling-overview.md).
## Background
There have been many proposals over time to improve error handling in Go. For instance, see:
* [golang.org/issue/21161](https://golang.org/issue/21161): simplify error handling with `|| err` suffix
* [golang.org/issue/18721](https://golang.org/issue/18721): add "must" operator `#` to check and return error
* [golang.org/issue/16225](https://golang.org/issue/16225): add functionality to remove repetitive `if err != nil` return
* [golang.org/issue/21182](https://golang.org/issue/21182): reduce noise in return statements that contain mostly zero values
* [golang.org/issue/19727](https://golang.org/issue/19727): add vet check for test of wrong `err` variable
* [golang.org/issue/19642](https://golang.org/issue/19642): define `_` on right-hand side of assignment as zero value
* [golang.org/issue/19991](https://golang.org/issue/19991): add built-in result type, like Rust, OCaml
Related, but not addressed by this proposal:
* [golang.org/issue/20803](https://golang.org/issue/20803): require call results to be used or explicitly gnored
* [golang.org/issue/19511](https://golang.org/issue/19511): “Writing Web Applications” ignores error from `ListenAndServe`
* [golang.org/issue/20148](https://golang.org/issue/20148): add vet check for missing test of returned error
We have also consulted the [experience reports about error handling](https://golang.org/wiki/ExperienceReports#error-handling).
Many of the proposals focus on verbosity.
both the verbosity of having to check error values
and the verbosity of zeroing out non-error return values.
Other proposals address issues related to correctness,
like error variable shadowing or the relative ease with which one can forget to check an error value.
This draft design incorporates many of the suggestions made in these issues.
## Design
This draft design builds upon the convention in Go programs
that a function that can fail
returns an `error` value as its final result.
This draft design introduces the keywords `check` and `handle`,
which we will introduce first by example.
Today, errors are commonly handled in Go using the following pattern:
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return err
}
y, err := strconv.Atoi(b)
if err != nil {
return err
}
fmt.Println("result:", x + y)
return nil
}
With the `check`/`handle` construct, we can instead write:
func printSum(a, b string) error {
handle err { return err }
x := check strconv.Atoi(a)
y := check strconv.Atoi(b)
fmt.Println("result:", x + y)
return nil
}
For each check, there is an implicit handler chain function,
explained in more detail below.
Here, the handler chain is the same for each check
and is defined by the single `handle` statement to be:
func handleChain(err error) error {
return err
}
The handler chain is only presented here as a function to define its semantics;
it is likely to be implemented differently inside the Go compiler.
### Checks
A `check` applies to an expression of type `error`
or a function call returning a list of values ending in
a value of type `error`.
If the error is non-nil.
A `check` returns from the enclosing function
by returning the result of invoking the handler chain
with the error value.
A `check` expression applied to a function call returning multiple results
evaluates to the result of that call with the final error result removed.
A `check` expression applied to a plain expression or to a function call returning only an error value
cannot itself be used as a value; it can only appear as an expression statement.
Given new variables `v1`, `v2`, ..., `vN`, `vErr`,
v1, ..., vN := check <expr>
is equivalent to:
v1, ..., vN, vErr := <expr>
if vErr != nil {
<error result> = handlerChain(vn)
return
}
where `vErr` must have type `error` and `<error result>` denotes
the (possibly unnamed) error result from the enclosing function.
Similarly,
foo(check <expr>)
is equivalent to:
v1, ..., vN, vErr := <expr>
if vErr != nil {
<error result> = handlerChain(vn)
return
}
foo(v1, ..., vN)
If the enclosing function has no final error result,
a failing `check` calls `handlerChain` followed by a return.
Since a `check` is an expression, we could write the `printSum` example above as:
func printSum(a, b string) error {
handle err { return err }
fmt.Println("result:", check strconv.Atoi(x) + check strconv.Atoi(y))
return nil
}
For purposes of order of evaluation, `check` expressions are treated as equivalent to function calls.
In general, the syntax of `check` is:
UnaryExpr = PrimaryExpr | unary_op UnaryExpr | CheckExpr .
CheckExpr = "check" UnaryExpr .
It is common for idiomatic Go code to wrap the error with context information.
Suppose our original example wrapped the error with the name of the function:
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
}
y, err := strconv.Atoi(b)
if err != nil {
return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
}
fmt.Println("result:", x+y)
return nil
}
Using a handler allows writing the wrapping just once:
func printSum(a, b string) error {
handle err {
return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
}
x := check strconv.Atoi(a)
y := check strconv.Atoi(b)
fmt.Println("result:", x + y)
return nil
}
It is not necessary to vary the wrapping code to determine where in `printSum` the error occurred:
The error returned by `strconv.Atoi` will include its argument.
This design encourages writing more idiomatic and cleaner error messages
and is in keeping with existing Go practice, at least in the standard library.
### Handlers
The `handle` statement defines a block, called a _handler_, to handle an error detected by a `check`.
A `return` statement in a handler
causes the enclosing function to return immediately with the given return values.
A `return` without values is only allowed if the enclosing function
has no results or uses named results.
In the latter case, the function returns with the current values
of those results.
The syntax for a `handle` statement is:
Statement = Declaration | … | DeferStmt | HandleStmt .
HandleStmt = "handle" identifier Block .
A _handler chain function_ takes an argument of type `error`
and has the same result signature as the function
for which it is defined.
It executes all handlers in lexical scope in reverse order of declaration
until one of them executes a `return` statement.
The identifier used in each `handle` statement
maps to the argument of the handler chain function.
Each check may have a different handler chain function
depending on the scope in which it is defined. For example, consider this function:
func process(user string, files chan string) (n int, err error) {
handle err { return 0, fmt.Errorf("process: %v", err) } // handler A
for i := 0; i < 3; i++ {
handle err { err = fmt.Errorf("attempt %d: %v", i, err) } // handler B
handle err { err = moreWrapping(err) } // handler C
check do(something()) // check 1: handler chain C, B, A
}
check do(somethingElse()) // check 2: handler chain A
}
Check 1, inside the loop, runs handlers C, B, and A, in that order.
Note that because `handle` is lexically scoped,
the handlers defined in the loop body do not accumulate
on each new iteration, in contrast to `defer`.
Check 2, at the end of the function, runs only handler A,
no matter how many times the loop executed.
It is a compile-time error for a handler chain function body to be empty:
there must be at least one handler, which may be a default handler.
As a consequence of what we have introduced so far:
- There is no way to resume control in the enclosing function after `check` detects an error.
- Any handler always executes before any deferred functions are executed.
- If the enclosing function has result parameters, it is a compile-time error if the handler chain for any check
is not guaranteed to execute a `return` statement.
A panic in a handler executes as if it occurred in the enclosing function.
### Default handler
All functions whose last result is of type `error` begin with an implicit _default handler_.
The default handler assigns the error argument to the last result and then returns,
using the other results unchanged.
In functions without named results, this means using zero values for the leading results.
In functions with named results, this means using the current values of those results.
Relying on the default handler, `printSum` can be rewritten as
func printSum(a, b string) error {
x := check strconv.Atoi(a)
y := check strconv.Atoi(b)
fmt.Println("result:", x + y)
return nil
}
The default handler eliminates one of the motivations for
[golang.org/issue/19642](https://golang.org/issue/19642)
(using `_` to mean a zero value, to make explicit error returns shorter).
In case of named return values,
the default handler does not guarantee the non-error return values will be zeroed:
the user may have assigned values to them earlier.
In this case it will still be necessary to specify the zero values explicitly,
but at least it will only have to be done once.
### Stack frame preservation
Some error handling packages, like [github.com/pkg/errors](https://github.com/pkg/errors),
decorate errors with stack traces.
To preserve the ability to provide this information,
a handler chain appears to the runtime
as if it were called by the enclosing function,
in its own stack frame.
The `check` expression appears in the stack
as the caller of the handler chain.
There should be some helper-like mechanism to allow skipping
over handler stack frames. This will allow code like
func TestFoo(t *testing.T) {
for _, tc := range testCases {
x, err := Foo(tc.a)
if err != nil {
t.Fatal(err)
}
y, err := Foo(tc.b)
if err != nil {
t.Fatal(err)
}
if x != y {
t.Errorf("Foo(%v) != Foo(%v)", tc.a, tc.b)
}
}
}
to be rewritten as:
func TestFoo(t *testing.T) {
handle err { t.Fatal(err) }
for _, tc := range testCases {
x := check Foo(tc.a)
y := check Foo(tc.b)
if x != y {
t.Errorf("Foo(%v) != Foo(%v)", tc.a, tc.b)
}
}
}
while keeping the error line information useful. Perhaps it would be enough to allow:
handle err {
t.Helper()
t.Fatal(err)
}
### Variable shadowing
The use of `check` avoids repeated declaration of variables named `err`,
which was the main motivation for
allowing a mix of new and predeclared variables in [short variable declarations](https://golang.org/ref/spec#Short_variable_declarations) (`:=` assignments).
Once `check` statements are available,
there would be so little valid redeclaration remaining
that we might be able to forbid shadowing
and close [issue 377](https://golang.org/issue/377).
### Examples
A good error message includes relevant context,
such as the function or method name and its arguments.
Allowing handlers to chain allows adding new information as the function progresses.
For example, consider this function:
func SortContents(w io.Writer, files []string) error {
handle err {
return fmt.Errorf("process: %v", err) // handler A
}
lines := []strings{}
for _, file := range files {
handle err {
return fmt.Errorf("read %s: %v ", file, err) // handler B
}
scan := bufio.NewScanner(check os.Open(file)) // check runs B on error
for scan.Scan() {
lines = append(lines, scan.Text())
}
check scan.Err() // check runs B on error
}
sort.Strings(lines)
for _, line := range lines {
check io.WriteString(w, line) // check runs A on error
}
}
The comments show which handlers are invoked for each of the
`check` expressions if these were to detect an error.
Here, only one handler is called in each case.
If handler B did not execute in a return statement,
it would transfer control to handler A.
If a `handle` body does not execute an explicit `return` statement,
the next earlier handler in lexical order runs:
type Error struct {
Func string
User string
Path string
Err error
}
func (e *Error) Error() string
func ProcessFiles(user string, files chan string) error {
e := Error{ Func: "ProcessFile", User: user}
handle err { e.Err = err; return &e } // handler A
u := check OpenUserInfo(user) // check 1
defer u.Close()
for file := range files {
handle err { e.Path = file } // handler B
check process(check os.Open(file)) // check 2
}
...
}
Here, if check 2 catches an error,
it will execute handler B and,
since handler B does not execute a `return` statement,
then handler A.
All handlers will be run before the `defer`.
Another key difference between `defer` and `handle`:
the second handler will be executed exactly once
only when the second `check` fails.
A `defer` in that same position would cause a new function call
to be deferred until function return for every iteration.
### Draft spec
The syntax for a `handle` statement is:
HandleStmt = "handle" identifier Block .
It declares a _handler_, which is a block of code with access to a new identifier
bound to a variable of type `error`.
A `return` statement in a handler returns from the enclosing function,
with the same semantics and restrictions as for `return` statements
in the enclosing function itself.
A _default handler_ is defined at the top of functions
whose last return parameter is of type `error`.
It returns the current values of all leading results
(zero values for unnamed results), and the error value
as its final result.
A _handler chain call_ for a statement and error value executes
all handlers in scope of that statement in reverse order in a new stack frame,
binding their identifier to the error value.
At least one handler must be in scope and, if the enclosing function
has result parameters, at least one of those (possibly the default handler)
must end with a terminating statement.
The syntax of the `check` expression is:
CheckExpr = "check" UnaryExpr .
It checks whether a plain expression or a function call’s last result,
which must be of type error, is non-nil.
If the error result is nil, the check evaluates to all but the last value.
If the error result is nil, the check calls its handler chain for that value
in a new stack frame and returns the result from the enclosing function.
The same rules that apply for the order of evaluation of calls in
expressions apply to the order of evaluation of multiple checks
appearing in a single expression.
The `check` expression cannot be used inside handlers.
## Summary
* A _handler chain_ is a function, defined within the context of an _enclosing function_, which:
- takes a single argument of type `error`,
- has the same return parameters as the enclosing function, and
- executes one or more blocks, called _handlers_.
* A `handle` statement declares a handler for a handler chain and declares
an identifier that refers to the error argument of that handler chain.
- A `return` statement in a handler causes the handler chain to stop executing
and the enclosing function to return using the specified return values.
- If the enclosing function has named result parameters,
a `return` statement with an empty expression list causes the handler chain
to return with the current values of those arguments.
* The `check` expression tests whether a plain expression or a
function’s last result, which must be of type `error`, is non-nil.
- For multi-valued expressions, `check` yields all but the last value as its result.
- If `check` is applied to a single error value,
`check` consumes that value and doesn’t produce any result.
Consequently it cannot be used in an expression.
- The _handler chain of a check_ is defined to execute all the handlers
in scope within the enclosing function in reverse order until one of them returns.
- For non-nil values, `check` calls the handler chain with this value,
sets the return values, if any, with the results, and returns from the enclosing function.
- The same rules that apply for the order of evaluation of calls in expressions
apply to the order of evaluation of multiple checks appearing in a single expression.
* A `check` expression cannot be used inside handlers.
* A _default handler_ is defined implicitly at the top of a function with a final result parameter
of type `error`.
- For functions with unnamed results, the default handler returns zero values
for all leading results and the error value for the final result.
- For functions with named results, the default handler returns the current
values of all leading results and the error value for the final result.
- Because the default handler is declared at the top of a function,
it is always last in the handler chain.
As a corollary of these rules:
* Because the handler chain is called like a function, the location
where the `check` caught an error is preserved as the handler’s caller’s frame.
* If the enclosing function has result parameters,
it is a compile-time error if at the point of any `check` expression
none of the handlers in scope is a
[terminating statement](https://golang.org/ref/spec#Terminating_statements).
Note that the default handler ends in a terminating statement.
* After a `check` detects an error, one cannot resume control of an enclosing function.
* If a handler executes, it is always before any `defer` defined within the same enclosing function.
## Discussion
One drawback of the presented design is that it introduces a context-dependent control-flow jump,
like `break` and `continue`.
The semantics of `handle` are similar to but the same as `defer`, adding
another thing for developers to learn.
We believe that the reduction in verbosity, coupled with the increased ease to wrap error messages
as well as doing so idiomatically is worth this cost.
Another drawback is that this design might appear to add exceptions to Go.
The two biggest problems with exceptions are
that checks are not explicitly marked and that
the invoked handler is difficult to determine
and may depend on the call stack.
`Check`/`handle` has neither problem:
checks are marked and only execute lexically scoped
handlers in the enclosing function.
## Other considerations
This section discusses aspects of the design that we have discussed in the past.
### Keyword: try versus check
Swift and Rust define a `try` keyword which is similar to the `check` discussed
in this design.
Unlike `try` in Swift and Rust, check allows checking of any expression
that is assignable to error, not just calls,
making the use of `try` somewhat contrived.
We could consider `try` for the sake of consistency with other languages,
but Rust is moving away from try to the new `?` operator,
and Swift has not just `try` but also `try!`, `try?`, `catch`, and `throw`.
### Keyword: handle versus catch
The keyword `handle` was chosen instead of `catch` to avoid confusion with the
exception semantics conventionally associated with `catch`.
Most notably, `catch` permits the surrounding function to continue,
while a handler cannot: the function will always exit after the handler chain completes.
All the handler chain can do is clean up and set the function results.
### Checking error returns from deferred calls
The presented design does not provide a mechanism for checking errors
returned by deferred calls.
We were unable to find a way to unify them cleanly.
This code does not compile:
func Greet(w io.WriteCloser) error {
defer func() {
check w.Close()
}()
fmt.Fprintf(w, "hello, world\n")
return nil
}
What the code likely intends is for the `check` to cause `Greet` to return the error,
but the `check` is not in `Greet`.
Instead, the `check` appears in a function literal returning no results.
The function therefore has no default handler,
so there is no handler chain for the `check` to call,
which causes a compilation failure.
Even with new syntax to write a deferred checked function call,
such as `defer check w.Close()`,
there is an ordering problem: deferred calls run
after the function executes its `return` statement;
in the case of an error, the handlers have already run.
It would be surprising to run any of them a second time
as a result of a deferred `check`.
### A check-else statement
A `check <expr> else <block>` statement could allow a block attached to
a check to be executed if an error is detected.
This would allow, for instance, setting an HTTP error code that a handler can pick up to wrap an error.
Joe Duffy proposed a similar construct in his
[Error Model](http://joeduffyblog.com/2016/02/07/the-error-model/) blog post.
However, this is generally not needed for error wrapping,
so it seems that this will not be needed much in practice.
Nesting `check` expressions with else blocks could make code unwieldy.
Analysis of a large code corpus shows that adding a `check`-`else`
construct usually does not help much.
Either way, the design does not preclude adding such a construct later if all else fails.
Note that a `check`-`else` can already be spelled out explicitly:
x, err := <expr>
if err != nil {
<any custom handling, possibly including "check err">
}
We can also write helpers like:
func e(err, code int, msg string) *appError {
if err == nil {
return nil
}
return &appError{err, msg, code}
}
check e(doX(), 404, "record not found")
instead of:
if err := doX(); err != nil {
return &appError{err, "record not found", 404}
}
Many wrapper functions, including `github.com/pkg/errors`'s `Wrap`,
start with a nil check.
We could rely on the compiler to optimize this particular case.
## Considered Ideas
### Using a ? operator instead of check
Rust is moving to a syntax of the form `<expr>?` instead of `try! <expr>`.
The rationale is that the `?` allows for better chaining, as in `f()?.g()?.h()`.
In Go, control flow transfers are as a general rule accompanied by keywords
(the exception being the boolean operators `||` and `&&`).
We believe that deviating from this would be too inconsistent.
Also, although the `?` approach may read better for chaining,
it reads worse for passing the result of a `check` to a function.
Compare, for instance
check io.Copy(w, check newReader(foo))
to
io.Copy(w, newReader(foo)?)?
Finally, handlers and `check` expressions go hand-in-hand.
Handlers are more naturally defined with a keyword.
It would be somewhat inconsistent
to have the accompanying `check` construct not also use a keyword.
## Comparisons
### Midori
Joe Duffy offers many valuable insights in the use of exceptions versus error codes
in his [Error Model](http://joeduffyblog.com/2016/02/07/the-error-model/) blog post.
### C++ proposal
Herb Sutter’s [proposal for C++](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf)
seems to come close to the what is presented here.
Although syntax varies in several places, the basic approach of propagating errors
as values with try and allowing handlers to deal with errors is similar.
The catch handlers, however, discard the error by default
unless they are rethrown in the catch block.
There is no way to continue after an error in our design.
The article offers interesting insights about the advantages of this approach.
### Rust
Rust originally defined `try!` as shorthand for checking an error
and returning it from the enclosing function if found.
For more complex handling, instead of handlers, Rust uses pattern matching on unwrapped return types.
### Swift
Swift defines a `try` keyword with somewhat similar semantics to the
`check` keyword introduced here.
A `try` in Swift may be accompanied by a `catch` block.
However, unlike with `check`-`handle`,
the `catch` block will prevent the function from returning
unless the block explicitly rethrows the error.
In the presented design, there is no way to stop exiting the function.
Swift also has a `try!`, which panics if an error is detected,
and a `try?`-`else`, which allows two blocks to be associated
that respectively will be run if the `try?` checks succeeds or fails.