| # 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 handling—found 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 Chen’s pair of blog posts, | |
| "[Cleaner, more elegant, and wrong](https://blogs.msdn.microsoft.com/oldnewthing/20040422-00/?p=39683)" (2004), | |
| and "[Cleaner, more elegant, and harder to recognize](https://blogs.msdn.microsoft.com/oldnewthing/20050114-00/?p=36693)" (2005). | |
| In essence, because you can’t 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 design’s `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**. | |
| We’ve 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. | |
| We’d 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 handling’s 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 design’s `check`. | |
| But Rust has no equivalent of `handle`: | |
| the convenience of the `?` operator comes with | |
| the likely omission of proper handling. | |
| Rust’s equivalent of Go’s 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. | |
| Rust’s `?` operator began life as | |
| [the `try!` macro](https://doc.rust-lang.org/beta/book/first-edition/error-handling.html#the-real-try-macro). | |
| ## Swift | |
| Swift’s `try`, `catch`, and `throw` keywords appear at first glance to be | |
| implementing exception handling, but really they are syntax for explicit error handling. | |
| Each function’s 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 Rust’s 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 Rust’s `.unwrap` method. | |
| Third, `try? canThrowErrors()` | |
| [evaluates to nil on error, or else the function’s 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. |