Skip to content

Instantly share code, notes, and snippets.

@eliben
Last active October 12, 2018 04:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eliben/d0c872054864bfc1110ec761c3c53c47 to your computer and use it in GitHub Desktop.
Save eliben/d0c872054864bfc1110ec761c3c53c47 to your computer and use it in GitHub Desktop.
Thoughts on the Go 2 Error Handling proposal

TL;DR: The proposal looks good, except the handler chaining part, which adds a lot of magical complexity to cater to rare use cases. Chaining can always be added at a later stage without breaking backwards compatibility, if its lack is deemed unbearable.

As I see it, the biggest issues in current Go usage the proposal tackles are:

  1. Repeated sequences of if err != nil {return nil, err} littering Go code
  2. Lack of proper context in propagated errors (see (1) above)

To fix these issues, I believe the proposed check keyword with a single handler per function are sufficient. Using stacks of handlers for proper cleanup is best left to defer, which is already a familiar tool. Thus, handle should only exist as a default "report more context in case of an error and return the error" mechanism. Where a single handler appears insufficient because the nature of error handling required changes through the function, the programmer should do some more abstracting (in the spirit of Errors are Values) or split their function to multiple smaller functions, each with its own error handler.

In other words, the proposal wants to solve the more general "resource management in light of problems" as well, which IMHO is a mistake since it creates too much complexity in the language.

More details: the need for check/handle

While I think the proposal should be modified, I'm strongly in favor of it otherwise. The example in the full proposal of taking this printSum function:

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

}

And writing it as:

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

}

Is very compelling because:

  1. The tedious repetitions of if err != nil ... are elided, while the check keyword still makes it clear that the return value of strconv.Atoi is checked for errors and something is being done about them.
  2. The error handler is a single place to add relevant context for the error.

On the other hand, the process example has scary complexity:

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

}

To understand what check 1 is doing we have to follow through three different handlers, figuring out exactly how the chaining of err is happening throughout them and what the final reported value is. For check 2 the sequence is different, and we have to carefully discern its lexical scope w.r.t the lexical scopes for handlers B and C to know they don't apply. In general, it adds quite a bit of mental burden to scan through the function looking for handlers and figuring out their lexical location in relation to the check - lots of action at a distance.

Moreover, the lexical chaining of handle is different from the runtime chaining of defer, which is very unintuitive.

For these reasons, I believe that chaining handlers add too much complexity to the language for too little gain. Resource management in light of errors is a genuinely hard problem, and doing it via lexically nested handlers just moves the complexity from one place to another. It would be better to leave it for programmers to solve in a more explicit way, so that visually tracing it doesn't require multiple independent flows of control. On the other hand, a simple single-handler approach can readily solve some of the most common issues with Go programming - if err != nil repetitions that break the logical flow of code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment