go: proposal: Go 2: The #id/catch error model, a rethink of check/handle
Please do not down-vote this post if you are against any new syntax for error handling.
Instead, vote in the first comment below. Thanks!
Having heard users’ frustrations with Go1 error handling, the Go team has committed to delivering a new method. Ideally, a solution would stem from a familiar language. The Go2 Draft Design is fine for wrapping errors with context, and returning them succinctly. But its feel is novel, and it has significant drawbacks, discussed in Golang, how dare you handle my checks!
Besides returning errors, Go programs commonly:
a) handle an error and continue the function that received it, and
b) have two or more kinds of recurring error handling in a single function, such as:
{ log.Println(err) }
{ debug.PrintStack(); log.Fatal(err) }
{ if err == io.EOF { break } }
{ conn.Write([]byte("oops: " + err.Error())) } // e.g. a network message processor
There is indeed a long list of Requirements to Consider for Go2 Error Handling. The check/handle scheme accommodates a tiny subset of these, necessitating an awkward mix of Go1 & Go2 idioms, e.g.
handle err { return fmt.Errorf(..., err) }
v, err := f()
if err != nil {
if isBad(err) {
check err // means 'throw'
}
// recover
}
Herein is a widely applicable approach to error handling, leveraging the C-family catch block. For the record, the author is grateful that Go does not provide C++ style exceptions, and this is not a plot to sneak them into the language through a side door 😃
The #id/catch Error Model
Let a catch identifier (catch-id) e.g. #err select a named handler. A single catch-id may appear in any assignment. A handler is known by its parameter name; the parameter can be of any type. A handler follows the catch-id(s) that trigger it and starts with catch <parameter>. Catch-ids are not variables and handler parameters are only visible within handlers, so there’s no re-declaration of error variables.
These are not unique ideas. At last count, 17 posts on the feedback wiki suggest various ways to define and invoke named handlers, and 13 posts suggest invocation of handlers using assignment syntax.
func (db *Db) GetX(data []byte) (int, error) {
n, #return := db.name() // return our own errors
f, #err := os.Open(n) // this file's presence is optional
defer f.Close()
_, #err = f.Seek(42, io.SeekStart)
l, #err := f.Read(data)
#return = db.process(data)
catch err error { // handle OS errors here
if !os.IsNotExist(err) { log.Fatal(err) }
log.Println(n, "not found; proceding")
}
#return = db.index(data) // executes unless catch exits
return l, nil
}
Several points are unresolved, see Open Questions below. Catch-id syntax is among them; #id is reminiscent of the URL form for goto id, but ?id, @id, and others are viable.
Advantages: similarity to established try/catch method (but without a try-block); clarity as to which handler is invoked for a given statement; certain statements may be skipped after an error occurs; handlers can return or continue the function.
Please help clarify (or fix) this proposal sketch, and describe your use cases for its features.
Feature Summary
Draft-2, 2018-09-19. Discussion following this comment below pertains to this draft.
These features meet a large subset of the Requirements to Consider for Go 2 Error Handling.
We can select one of several distinct handlers:
func f() error {
v1, #fat := fatalIfError() // a non-zero value for #id triggers corresponding catch
v2, #wrt := writeIfError()
// predefined handlers
v3, #_ := ignoreIfError() // or log the error in debug mode
v4, #r := returnIfError() // aka #return
v5, #p := panicIfError() // aka #panic
catch fat { log.Fatal(fat) } // inferred parameter type
catch wrt error { conn.Write(wrt.Error()) }
}
We can invoke a handler defined at package level (thanks @8lall0):
func f() error {
#pkg = x()
}
catch pkg error { // package-level handler; explicit type
log.Println(pkg)
return pkg // return signature must match function invoking pkg handler
}
We can specify a type for implicit type assertion:
f := func() error { return MyError{} }
#err = f()
catch err MyError { ... }
We can skip statements on error and continue after the handler:
#err = f()
x(1) // not called on error
catch err { log.Println(err) }
x(2) // always called
We can forward to a different handler (creates an explicit handler chain):
#ret = x()
if ... {
#err = f()
catch err {
if ... { #ret = err } // invoke alternate handler
#ret = fmt.Errorf(..., err) // invoke handler with alternate input
}
}
catch ret { ... }
We can reuse catch-ids:
#err = f(1)
catch err { ... }
#err = f(2)
catch err { ... }
We can nest catch blocks:
#era = f(1)
catch era {
#erb = f(2)
catch erb { ... } // cannot use 'era'; shadowing in catch disallowed
}
We can see everything from the scope where a handler is defined, like closure functions:
v1 := 1
if t {
v2 := 2
#err = f()
catch err { x(v1, v2) }
}
We can still use Go1 error handling:
v1, err := x() // OK
v2, err := y() // but re-declaration might be abolished!
Open Questions
-
What catch-id syntax?
#id,?id,@id,id!,$id, … What style for predefined handlers?#r,#p,#_,#return,#panic,#nil, … -
What handler definition syntax?
catch id [type],catch id(v type),id: catch v [type], … Infer parameter from previous stmt?#err = f(); catch { log.Println(err) } -
Invoke handler when ok=false for
v, #ok := m[k]|x.(T)|<-c, etc? Pass a typeerrorwith context?v, #err := m[k]; catch { log.Println(err) } -
Treat parameter as const?
catch err { err = nil } // compiler complainsLets forwarding skip test for nil:catch err { #ret = err } -
Require
#idfor return values of typeerror? #20803 -
Provide
checkfunctionality withf#id()? e.g.x(f1#_(), f2#err())
If so, disallow nesting?x(f1#err(f2#err()))
Allow position selector?f#id.0()tests first return value -
Provide more context to package-level handlers, e.g. caller name, arguments?
catch (pkg error, caller string) { ... } -
Allow handlers in defer stack?
defer last() // skip if handler returns defer catch errd { ... } defer next#errd() // skip if first() invokes handler defer first#errd() -
Allow multiple handler arguments?
#val, #err = f() // return values assignable to catch parameter types catch (val T, err error) { ... } // either parameter could be non-zero
Disallowed Constructs
Declaring or reading a catch-id:
var #err error // compiler complains
#err = f()
if #err != nil { ... } // compiler complains
catch err { ... }
Multiple catch-ids per statement:
#val, #err = f() // compiler complains
catch val { ... } // if f() returns two non-zero values, which handler is executed?
catch err { ... }
Shadowing of local variables in handlers:
func f() {
if t {
err := 2
#err = f() // OK; #err handler can't see this scope
}
pkg := 1 // OK; #pkg handler (see above) can't see local variables
err := 1
#err = f()
catch err { return err } // compiler complains; err==1 is shadowed
}
Self-invocation:
#err = f(1)
catch err {
#err = f(2) // compiler complains
}
#err = f(3)
catch err { ... }
Unused handlers:
catch err { ... } // compiler complains
#err = f()
catch err { ... }
#ret = f()
catch err { return err } // compiler complains
catch ret { ... }
catch ret { return ret } // compiler complains
Discarded Ideas
Chain handlers with same catch-id in related scopes implicitly, as in the Draft Design:
func f() {
v, #fat := x()
if v != nice { // new scope
#fat = y(&v)
catch fat { // invoked 1st
if ... { #fat = nil } // can skip other handlers in chain
} // no return/exit, continue along chain
}
catch fat { log.Fatal(fat) } // invoked 2nd
}
Changelog
2018-09-19 draft-2 (discussion below)
a) Move implicit handler chain to new section “Discarded Ideas”.
b) Make continuing after catch the default behavior.
c) Document catch-id reuse and nested catch block.
d) Disallow unused handlers (was “contiguous handlers with same catch-id”) and self-invocation.
e) Add #_ predefined handler to ignore or log input.
f) Add implicit type assertion.
Why Not check/handle?
Please read Golang, how dare you handle my checks! for a discussion of each of the following points.
- No support for multiple distinct handlers.
- The last-in-first-out
handlechain cannot continue a function. checkis specific to typeerrorand the last return value.- The per-call unary
checkoperator can foster unreadable constructions. - The default handler makes it trivial to return errors without context.
- Handlers appear before the calls that trigger them, not in the order of operations.
- The
handlechain is inapparent; one must parse a function by eye to discover it.
Also, there is relatively little support for the draft design on the feedback wiki.
Named Handlers Are Popular!
At last count, roughly 1/3rd of posts on the feedback wiki suggest ways to select one of several handlers:
- @didenko github
- @forstmeier gist
- @mcluseau gist
- @the-gigi gist
- @PeterRK gist
- @marlonche gist
- @alnkapa github
- @pdk medium
- @gregwebs gist
- @gooid github
- @spakin gist
- @morikuni gist
- @AndrewWPhillips blogspot
- @bserdar gist
- @martinrode medium
- @dpremus gist
- @networkimprov this page
And the following posts suggest ways to invoke a handler with assignment syntax:
- @oktalz gist
- @pborman gist
- @kd6ify blog
- @rockmenjack github
- @the-gigi gist
- @8lall0 gist
- @dpremus gist
- @bserdar gist
- @mcluseau gist
- @didenko github
- @gooid github
- @Kiura gist
- @networkimprov this page
/cc @rsc @mpvl @griesemer @ianlancetaylor @8lall0 @sdwarwick @kalexmills
Thanks for your consideration,
Liam Breck
Menlo Park, CA, USA
About this issue
- Original URL
- State: open
- Created 6 years ago
- Reactions: 43
- Comments: 44 (5 by maintainers)
Go error handling works fine today, no new
errorspecific syntax is necessary.Some new general-purpose features (macros?) would ease error handling.
hello,
Go handle errors the perfect way.
it is 100% clear.
zero problems with
if err != nil { panic(err) }
regards! Valdemar
I don’t like some kind of “Error-Handle”, especially:
It is hard to find which handle will catch the error unless count the code above. And it is not possible to specify the error handle.
I encourage you to paste your critique of check/handle into a gist and list it on the feedback wiki.
We agree that
if err != nil {...}is effective in many cases. But when constantly calling an API where most calls yielderror(e.g. os.File), the 3 lines of boilerplate for every call decreases the program’s density to the point where it’s inefficient for the reader; code density matters. The Go team has decided to seek a fix for this.Given that decision, we need the set of requirements for any solution. The Go team has not compiled one, but I have in Requirements to Consider for Go2 Error Handling. All the terms therein may not be essential, but virtually all of them are satisfied by two features:
#name- invoke a handler by namecatch name {...}- define a handlerThat’s pretty easy to explain, and far easier to read when
errorresults fall from most lines in a function 😃Mix of Go1 & Go2 idioms is not a problem.
personnaly, I prefer having a clear keyword instead of increasing the semantic load on something else. The check keyword appeared many times, inline sgtm too. My preferred way is
I like proposal with:
#name- invoke a handler by namecatch name {...}- define a handlerIt solved many situations, but i think can be improved with panic and return state, like in proposal:
Because it is will be decreased user code(user need write this manual) in all situations, and code will be clear and understandable, and this will solve error tracing
I would not introduce special symbols in the language.
#err, !err, ?err, ...Go marks exported fields and functions with capital letters (log.Fatal is exported, log.innerFuction is not exported).
This would be a nicer way to go:
In other words, if the variable is “fatal”, “ret”, “stderr”, … and starts with an upper case letter, then it is an #err id.
Surely this idea needs more work, but I believe the point of not introducing symbols (non word characters) in the language is important.
Re
goto, i==0, but go vet should maybe flag it. Compiler error works too.The trace diagram is delightful but inaccurate. “Resume” means continue execution after the catch block. Which is how catch works in other languages. I’ll clarify the Feature Summary on that point. (But note that the behavior you imagined is similar to using a locally-defined function 😃
It’s not “fixed”, it’s just expanded upon. You can now declare variables under that construct, but you can’t use them outside of the range between the
goto lbland thelbl:. My example still should not compile, even though it looks intuitive.Assuming you mean “flagged” as in give a compiler error (as there are no warnings in Go), would the following be illegal?
Since
catch errreturns, it needs to be placed immediately after… So since there is code between theoops()call and thecatch, it should fail to compile, correct? I’m not sure I’m understanding this.If so, then all you’ve done is replaced
if err != nilwithcatch err, which is definitely not a good solution.Okay, so I understand the flow now, but I’m not sure I like it. In order to understand it, my eyes need to dart all around the function in order to follow it, when it should be going a single direction.
The nice thing about
check/handlewas that the function went downward as normal, but then once an error occurs, it flows (block-wise) upward.Going back to the original draft -
I don’t like this - I want my returns to be within the function… a function’s flow should not be determined by something outside of the function (ever). Also, how would this work with multi-value returns?
Again, my main gripe with the
#id/catchproposal is that it gives the illusion that the function is running from the top-down. Thecheck/handlevery obviously shows that it does NOT run top-down, because thehandlerfor an error needs to be declared above thecheck. I do like the concept of a named error handler, but I’m not sure that this is the way to do it. (Sorry if I’m coming off as mean or something, just trying to give constructive criticism)A major issue with this is the same as with
gotos.Currently,
gotogets around this by saying no variables may be defined inside any region betweengoto lblandlbl:(although there are a couple proposals that are refining this to make things a bit easier)Also, what about this?
What order do these print in? Because it looks like the program runs top-down, does it print
1 3, or1 2 4? I think thecheck/handleconstruct made this a bit clearer since it was extremely clear that the program does not run top-down, but because in this example, since thecatches are defined below the#ids, it makes it a lot more difficult to recognize the flow of the program just by looking at it.