Instantly share code, notes, and snippets.

Embed
What would you like to do?
Requirements to Consider for Go 2 Error Handling

Requirements to Consider for Go 2 Error Handling

Towards consensus on the requirements for a new errors idiom.

The Go2 Error Handling Overview gives only four rather vague "goals": small-footprint error checks, developer-friendly error handlers, explicit checks & handlers, and compatibility with existing code. And previously @ianlancetaylor made this similar list: https://github.com/golang/go/issues/21161#issuecomment-389380686

As of this writing, there are almost forty counter-proposals on the feedback wiki for the Go 2 Error Handling Draft Design. Almost none of them discuss the requirements that motivate them, but many imply requirements that the draft design does not fulfill. The Go team might benefit from a consensus on requirements before penning a next draft.

Below is a comprehensive list of possible requirements. Please comment with any others that should be considered.

PS: For those wondering why I've taken the trouble, it's because I had this reaction to the draft design :-)

The List

A. Allow Go1 error idiom to continue working if err != nil { ... }

B. Concise, reusable error "handlers" within a function and/or package.

  1. Uniquely identify handlers, e.g.

    handle h T { return h }     // id is parameter name
    handle h(p T) { return p }  // id is handler name
    h: handle p T { return p }  // id is label
    
  2. Let handler body contain any code that's valid in the enclosing function.

  3. Let handler parameter be of any type, or only type error

    3.1 Perform implicit type assertion if parameter type implements an interface appearing in invocation of handler.

  4. Invoke handler body on any parameter value indicating error.
    Or, for a slice parameter type, on any element value indicating error.

  5. Permit handlers for deferred function calls, e.g.
    defer handle h T { return h }

C. Concise, clear way to select a handler by name. >1/3rd of wiki posts suggest this.

  1. Let assignment invoke a handler.

  2. Let function call invoke a handler.

  3. Define syntax to select a named handler. Options:

    // op: keyword or symbol (e.g. ? @ # and unambiguous pairings)
    //     space between symbol and identifier is not required
    // hname: handler name
    
    v, hname op := f(a)   // assignment
    v, op hname := f(a)
    
    v := op(f(a), hname)  // function call
    v := op(hname, f(a))
    v := f(a) op hname
    v := hname op f(a)
    v := f op hname (a) // n.b.
    

D. Let name of unreachable handler be reused, e.g.

op hname = f(1)
handle hname T { ... }
op hname = f(2)
handle hname T { ... }

E. Let function returning error and a value serve as expression, e.g.

f := func(i int) (int, error) { ... }
x(f op hname (a))
f op hname (a).x()
handle hname error { ... }
  1. Let return value passed to handler default to the last one.

  2. Let index of return value passed to handler be specified, e.g.
    x(f op hname . digit (a))

F. Invoke handler on boolean false parameter for

v, op hname := m[k]
v, op hname := x.(T)
v, op hname := <-c
  1. Generate meaningful parameter value, e.g.
    v, op hname := m[k]; handle hname error { return hname }

G. Let handling of an error skip over any statements between the statement triggering the handler and the handler body. (Entails placement of handler after statements that invoke it.) e.g.

op hname = f()
skipOnError()
handle hname T { ... }
  1. Let handling of an error from a deferred function call skip over any defer statements between the trigger and handler body, e.g.
    defer handle hname T { ... } // triggered by f()
    defer skipOnError()
    defer f op hname ()
    

H. Let handler continue the function. (Entails placement of handler after statements that invoke it.) e.g.

{ if err == io.EOF { break } } // stop enclosing loop, etc
  1. Let a special statement exit the handler, e.g.
    { if ... { break op hname } }

  2. Let a handler invoked by a deferred function call continue up the defer stack, e.g.

    defer runLast()
    defer handle hname T { ... } // triggered by f()
    defer f op hname ()
    

I. Let handler invoke another handler explicitly, e.g.

{ if err != io.EOF { op quit = err } } // invoke "quit" handler
  1. Let handler invoke another handler implicitly, e.g. handlers with same name in related scopes.

J. Let handler perform context accretion, e.g.

{ op quit = fmt.Errorf("blurb: %v", err) }

K. Permit package-level handlers, essentially a custom "predefined handler", e.g.

package main
func f() {
   op hname = x()
}
handle hname T { ... }
  1. Provide caller name to handler, e.g.
    handle (hname T, caller string) { ... }

L. Provide predefined handlers, e.g.

op return = f() // return on error
op panic  = f() // panic on error
op _      = f() // ignore error; for expression f op _ (a)
  1. Disallow the return handler in functions which don't define the requisite type as the last return value.

  2. Goroutine functions and main.main shall not return a value.

  3. Let the ignore handler log errors or all values in a debug mode.

M. Calls to functions returning type error shall invoke a handler for the error value, or assign it to a variable (i.e. not _).

op _ = f() // OK
 err = f() // OK
   _ = f() // compile error
       f() // compile error

N. Define keywords for handle and possibly op above. Consider any, or only C-family, or only Go1 keywords.

@Kiura

This comment has been minimized.

Show comment
Hide comment
@Kiura

Kiura Sep 27, 2018

Adding try catch would make it much easier for developers I believe. Java has done a great job in that area.
an example of how this could be achieved

This seems to be better option to wrap the code in try catch that needs to be handled rather than creating named handle functions.
Although, package-level handlers sounds cool, it seems like it is gonna make error handling more implicit

Kiura commented Sep 27, 2018

Adding try catch would make it much easier for developers I believe. Java has done a great job in that area.
an example of how this could be achieved

This seems to be better option to wrap the code in try catch that needs to be handled rather than creating named handle functions.
Although, package-level handlers sounds cool, it seems like it is gonna make error handling more implicit

@networkimprov

This comment has been minimized.

Show comment
Hide comment
@networkimprov

networkimprov Sep 28, 2018

Go will never offer a try-block; it was deliberately excluded. It obscures the connection between an error's cause and effect, and the extra indent is unpopular. Go 2 will probably offer a per-call try, e.g. check f() or #hname = f() or f?hname().

Owner

networkimprov commented Sep 28, 2018

Go will never offer a try-block; it was deliberately excluded. It obscures the connection between an error's cause and effect, and the extra indent is unpopular. Go 2 will probably offer a per-call try, e.g. check f() or #hname = f() or f?hname().

@pkutishch

This comment has been minimized.

Show comment
Hide comment
@pkutishch

pkutishch Sep 28, 2018

Why don't just not touch error handling part? I love if err != nil

pkutishch commented Sep 28, 2018

Why don't just not touch error handling part? I love if err != nil

@networkimprov

This comment has been minimized.

Show comment
Hide comment
@networkimprov

networkimprov Sep 28, 2018

You could write the following, but it would be poor style :-)

v, err := f()
if err != nil { #hname = err }
...
handle hname error { ... }
Owner

networkimprov commented Sep 28, 2018

You could write the following, but it would be poor style :-)

v, err := f()
if err != nil { #hname = err }
...
handle hname error { ... }
@mewmew

This comment has been minimized.

Show comment
Hide comment
@mewmew

mewmew Oct 1, 2018

There are two issues with Go error handling today, which Go 2 may set out to solve.

  1. multiple returns for error handling prevents the use of chaining in Go 1
  2. checking errors of deferred functions is somewhat involved.

Chaining example

Often there are precisely two return values of a function or method, with the last one being used for error handling. If using check and handle (or some other mechanism) it would be possible to use chaining, that would enable new ways of writing code.

// go1
	t, err := F()
	if err != nil { ... }
	_, err := t.M()
	if err != nil { ... }

// go2
	_ = F().M()       // Would it be possible to do this?
	_ = check F().M() // Or with check?

Defer error check example

Defer is often used to cleanup resources after use, e.g. Close to close a file descriptor. However, in practice defer often seem to be used without taking into consideration the potential error return value. Sometimes this value is ignored for good reasons (there is nothing you intend to do with it). Other times, at least reporting the error to the user (close file foo failed), or better yet, trying to flush any pending writes, or write the file contents to a new file to ensure that no file contents is lost, may be preferable.

Searching in the Go standard library reveals quite a few such cases (grep -R -e "defer [a-zA-Z0-9_]\.Close" ~/go1.11/src/), e.g. from src/cmd/gofmt/gofmt.go:

func diff(b1, b2 []byte, filename string) (data []byte, err error) {
	f1, err := writeTempFile("", "gofmt", b1)
	if err != nil {
		return
	}
	defer os.Remove(f1) // <-- error not checked.

Other parts of the standard library implements custom error handling for checking errors of deferred functions, e.g. from src/testing/cover.go:

defer func() { mustBeNil(f.Close()) }()

mewmew commented Oct 1, 2018

There are two issues with Go error handling today, which Go 2 may set out to solve.

  1. multiple returns for error handling prevents the use of chaining in Go 1
  2. checking errors of deferred functions is somewhat involved.

Chaining example

Often there are precisely two return values of a function or method, with the last one being used for error handling. If using check and handle (or some other mechanism) it would be possible to use chaining, that would enable new ways of writing code.

// go1
	t, err := F()
	if err != nil { ... }
	_, err := t.M()
	if err != nil { ... }

// go2
	_ = F().M()       // Would it be possible to do this?
	_ = check F().M() // Or with check?

Defer error check example

Defer is often used to cleanup resources after use, e.g. Close to close a file descriptor. However, in practice defer often seem to be used without taking into consideration the potential error return value. Sometimes this value is ignored for good reasons (there is nothing you intend to do with it). Other times, at least reporting the error to the user (close file foo failed), or better yet, trying to flush any pending writes, or write the file contents to a new file to ensure that no file contents is lost, may be preferable.

Searching in the Go standard library reveals quite a few such cases (grep -R -e "defer [a-zA-Z0-9_]\.Close" ~/go1.11/src/), e.g. from src/cmd/gofmt/gofmt.go:

func diff(b1, b2 []byte, filename string) (data []byte, err error) {
	f1, err := writeTempFile("", "gofmt", b1)
	if err != nil {
		return
	}
	defer os.Remove(f1) // <-- error not checked.

Other parts of the standard library implements custom error handling for checking errors of deferred functions, e.g. from src/testing/cover.go:

defer func() { mustBeNil(f.Close()) }()
@networkimprov

This comment has been minimized.

Show comment
Hide comment
@networkimprov

networkimprov Oct 1, 2018

Thank you for raising the defer issue! I've added items in B-5, G-1, and H-2. Note that without them, one could use

defer func() { op hname = f(); handle hname T { ... } }()

defer f op return () // predefined handlers
defer f op _ ()

Method chaining is covered by E, but I've added a line to its example with a method chain. Note that the check solution is unreadable:

check (check F()).M()

I need to edit your comment a little for brevity; pls don't take that as criticism :-)

Owner

networkimprov commented Oct 1, 2018

Thank you for raising the defer issue! I've added items in B-5, G-1, and H-2. Note that without them, one could use

defer func() { op hname = f(); handle hname T { ... } }()

defer f op return () // predefined handlers
defer f op _ ()

Method chaining is covered by E, but I've added a line to its example with a method chain. Note that the check solution is unreadable:

check (check F()).M()

I need to edit your comment a little for brevity; pls don't take that as criticism :-)

@daved

This comment has been minimized.

Show comment
Hide comment
@daved

daved Oct 9, 2018

@mewmew
Chaining should normally be limited to accumulators and accumulator-like types. An error being returned is a great indication that chaining is not a good idea with that particular type.

When an error is returned by a construction function (or similar), it would be reasonable for the called scope to cleanup after itself. Requiring users to cleanup something that failed is unintuitive.

daved commented Oct 9, 2018

@mewmew
Chaining should normally be limited to accumulators and accumulator-like types. An error being returned is a great indication that chaining is not a good idea with that particular type.

When an error is returned by a construction function (or similar), it would be reasonable for the called scope to cleanup after itself. Requiring users to cleanup something that failed is unintuitive.

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