Now posted on the golang issue tracker, #27519.
Appreciations to the Go team for presenting a thoughtful and detailed set of Go2 draft designs. The Error Handling Draft is great for logging errors via the handler chain, and returning them succinctly. However, 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.Error()); return err }
{ log.Fatal(err) }
{ if err == io.EOF { break } }
{ conn.Write([]byte("oops: " + err.Error())) } // e.g. a network message processor
The check/handle
scheme doesn't accommodate these patterns, necessitating an awkward mix of Go1 & Go2 idioms:
if err = f(); err != nil {
if isSerious(err) { check err }
// handle recoverable error
}
And there are other problems, outlined in Why Not check/handle below.
func (db *Db) GetX(data []byte) (int, error) {
n, #_ := db.name() // return our own errors via default handler
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)
#_ = db.process(data)
catch err error { // handle OS errors here
if !os.IsNotExist(err) { log.Fatal(err) }
log.Println(n, "not found; proceding")
#err = nil // resume (should be the default?)
}
return l, nil
}
Here a catch identifier (catch-id) e.g. #err
selects an error 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.
Handlers with the same parameter name in related scopes form a chain.
A handler follows the catch-id(s) that trigger it and starts with keyword catch
.
Catch-ids are not variables and handler parameters are only visible within handlers,
so there's no re-declaration of error variables.
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.
Please help clarify (or fix) this proposal sketch, and describe your use cases for its features.
We can select one of several distinct handlers:
func f() error {
v1, #fat := fatalIfError() // a non-zero value for #id triggers the corresponding catch
v2, #wrt := writeIfError()
v3, #_ := returnIfError() // default handler, returns the value if triggered
v4, #! := panicIfError() // default handler, panics if triggered; for callers that don't return
catch fat error { log.Fatal(fat) } // if type is error, type name could be optional
catch wrt error { con.Write(...); return nil } // return/exit required in last handler on chain
}
We can chain handlers across scopes, as in the Error Handling Draft:
func f() {
v, #fat := x()
if v != nice { // new scope
#fat = y(&v)
catch fat error { debug.PrintStack() } // no return/exit; chained with next catch fat
}
catch fat error { log.Fatal(fat) } // parameter types must be the same across the chain
}
We can invoke a handler defined at package level (thanks @8lall0):
func f() error {
#pkg = x()
catch pkg { ... } // optional
}
catch pkg { // package-level handler
log.Println(pkg.Error())
return pkg // return signature must match function invoking pkg handler
}
We can cancel the chain and resume after the handler:
#err = f()
catch err { log.Println(...); #err = nil } // exit handler; don't goto next handler in chain
// can be the only/last handler in chain
x() // called after handler
We can change the input for the next chained handler:
if t {
#err = f()
catch err {
if ... {
#err = MyError{...} // exit handler; set input for next handler in chain
} else {
err = nil // no effect on next handler
}
}
}
catch err { return err } // if missing, compiler complains about self-invocation
We can forward to a different handler chain (can be applied to create an explicit chain in lieu of an implicit one):
if ... {
#err = f()
catch err { if ... { #ret = err } } // exit handler; pass err to handler in same or outer scope
// err must be non-zero
}
catch ret { ... }
catch err { ... } // may not be called
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!
- What catch-id syntax?
#id
,?id
,@id
,id!
,$id
, ... - What catch-id for default handlers?
#_
,#!
,#default
,#panic
, ... - What handler definition syntax?
catch id
,catch id type
,catch (id type)
,except id
, ... - Require
#id
or_
for return values of typeerror
? - Provide
check
functionality withf#id()
? e.g.x(f1#_(), f2#err())
If so, disallow nesting?x(f1#err(f2#err()))
- Allow unused handler?
func f() { catch ret { ... } // unused #ret = x() catch ret { ... } // OK; preceding catch-id selects this catch err { ... } // unused }
- Drop implicit handler chaining? (Handler default action would be resume.)
if ... { #err = f1() catch err { log.Println(...) } } #err = f2() catch err { return nil } // sees errors from f1 & f2; was f2 the only intended source?
- Provide more context to package-level handlers, e.g. caller name, arguments?
catch pkg, caller { log.Println(caller, pkg.Error()) }
- Allow multiple handler arguments?
#val, #err = f() // return values must be assignable to catch parameter types catch val T, err error { ... } // either parameter could be non-zero
Reading a catch-id:
#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
}
Contiguous handlers with the same catch-id and scope:
#err = f()
catch err { ... }
#ret = f()
catch err { return err } // compiler complains
catch ret { ... }
catch ret { return ret } // compiler complains
check
is specific to typeerror
and the last return value.check
doesn't support multiple distinct handlers.- Function call nesting with a per-call unary operator can foster unreadable constructions:
f1(v1, check f2(check f3(check f4(v4), v3), check f5(v5)))
May I remind you, we are forbidden this:f(t ? a : b)
- The outside-in
handle
chain can only bail out of a function. - Handlers that add context should appear after the relevant calls, in the order of operations:
for ... { #err = f() catch err { #err = fmt.Errorf("loop: %s", err.Error()) } } catch err { return fmt.Errorf("context: %s", err.Error()) }
- There is relatively little support for the draft design on the feedback wiki.
At last count, more than 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
- @networkimprov this page
/cc @rsc @mpvl @griesemer @ianlancetaylor @8lall0 @sdwarwick @kalexmills
@gopherbot add Go2 LanguageChange Proposal
Thanks for your consideration,
Liam Breck
Menlo Park, CA, USA
@8lall0, thanks for your input! Great idea to allow package-level
catch pkgVar error {...}
! I've added that to my text.I suggested catches at top or bottom of scope, but not anywhere. I want them at the bottom because a handler could be long, and I find it odd to read what to do last at the top of a scope. However that could be mitigated by allowing local-scope
func f() {...}
(i.e. notf := func(){...}
) to appear anywhere in a function and be callable before it appears.