go: proposal: Go 2: block scoped error handling
After reading through the problem overview the try() proposal and every comment on the try() issue https://github.com/golang/go/issues/32437 I like the idea of try(), thank you everyone for such hard work! The implementations so far have not solved what I view as the issue of error boilerplate, namely the scope of the err var.
Problem
Go scopes errors using inline if statements, but does not scope errors when a variable needs to be used after.
Scoped:
if err := foo(); err != nil {
if _, err := bar(); err != nil {
Not Scoped:
r, err := bar()
if err != nil {
When the error is not scoped it is has multiple problems. The ones that come to mind are:
- Shadowing
- All
errs need to be the same type - The variable existing past when handling makes sense
Proposal
I propose the language add try/handle keywords similar to what was proposed by @james-lawrence.
r := try bar() handle(err error) {
return err
}
The try keyword would return everything but the final value. The handle block would only run if the final value was non-zero.
This proposal also solves many common complaints from the try proposal https://github.com/golang/go/issues/32437:
- Requiring a defer function
- Requiring a named returned which some developers try to avoid
- Implicit error handling
- Obscured
return - Adding complexity to error decoration
Examples
CopyFile
The CopyFile func found in the overview becomes
func CopyFile(src, dst string) error {
r := try os.Open(src) handle(err error) {
return err
}
defer try r.Close() handle(err error) {
//handle
}
w := try os.Create(dst) handle(err error) {
return err
}
defer try w.Close() handle(err error) {
//handle
}
try io.Copy(w, r); handle(err error) {
return err
}
try w.Close(); handle(err error) {
return err
}
}
The main difference between current error boilerplate and using block scoped error handling are:
- The
errfromos.Open(src)doesn’t live for the whole scope of the function - The
deferfunctions can handle their errors in the same manner as other functions
Hex
The hex example in the overview becomes:
func main() {
hex := try ioutil.ReadAll(os.Stdin) handle(err error) {
log.Fatal(err)
}
data := try parseHexdump(string(hex)) handle(err error) {
log.Fatal(err)
}
os.Stdout.Write(data)
}
This is very similar to the current go except the err returned from ioutil.ReadAll isn’t overwritten by the err from parseHexdump.
Inline/Curried functions
The try proposal comments included examples of nested/curried functions that caused some worry such as this AsCommit.
func AsCommit() error {
return try(try(try(tail()).find()).auth())
}
Wrapping a try/handle in () would mean a variable never needs to be saved if it is not wanted. This case is similar to using an anonymous func func()int { ... }() and handling the error, but would allow use of other keywords like break, continue, and return.
Current Go
func AsCommit() error {
t, err := tail()
if err != nil {
return err
}
f, err := t.find()
if err != nil {
return err
}
if _, err := f.auth(); err != nil {
return err
}
}
With block scoped error handling (please don’t do this)
func AsCommit() error {
(try tail() handle(err error) {
return err
}).(try find() handle(err error) {
return err
}).(try auth() handle(err error) {
return err
})
}
Struct Init
type foo struct {
Value int
}
Current go
func styleA(s string) error {
f := foo{}
var err error
f.Value, err = strconv.Atoi(s)
if err != nil {
return errors.Wrap(err, "value could not be converted")
}
func styleB(s string) error {
n, err := strconv.Atoi(s)
if err != nil {
return errors.Wrap(err, "value could not be converted")
}
f := foo{
Value: n,
}
With block scoped error handling
func styleB(s string) error {
f := foo{}
f.Value: try strconv.Atoi(s) handle(err error) {
return errors.Wrap(err, "value could not be converted")
}
func styleA(s string) error {
f := foo{
Value: (try strconv.Atoi(s) handle(err error) {
return errors.Wrap(err, "value could not be converted")
}),
}
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 25
- Comments: 22 (6 by maintainers)
Except for the scoping, there’s really not much difference between
and the current approach:
In fact, the
try-handleapproach is more verbose by a few characters, requires declaration of the error type, and understanding yet another major language mechanism/syntax (rather than a helper function such astry) solely for error handling. The use of two keywords is not very economical either given that this is a very specialized statement.It is very easy to come up with new control flow structures for any language - but it is hard to come up with control flow statements that are universally useful and add significant power. This construct does not add power over what we already have, nor is it universally useful. I don’t believe we should add two new keywords and a whole new statement for something we can already write in Go.
There seems to be a magic variable “err” in the handle block. Where was it defined?
I proposed this under the name
guard, based on Swift’sguardkeyword. That might have been in the old issue beforetry().Some thoughts:
guardbecause that’s a name used by another language.ifto be adopted.@mvndaai The point you are making about
errexisting past when it should is a point one could make about any value that is returned by a function and only needed in a subsequent test and perhaps for areturn- it’s not unique toerror error handling for that matter. There are plenty of functions that return aboolto indicate success. I can imagine plenty of scenarios where a function returns multiple values, with one of those values tested to decide whether to continue or return (and where that value is not used anymore even of the function continues).In other words, the mechanism you are introducing would be far more interesting if it could be used in a variety of scenarios, and if it “just happens” to also work well for errors; especially if the mechanism would also be dead simple. Such a mechanism would amortize the cost and complexity it adds to the language by being more universally useful.
That more universal construct seems to be a plain
ifstatement. Together with redeclaration in Go, there is really no issue witherrliving past its first use, so it can be re-used again and again with otherif err != nilchecks. While it would be “nice” to restrict the scope oferr, it doesn’t seem to solve an urgent issue.@mvndaai Yes. With
errors.Is, the boilerplate is still there but somehow part of the problem is solved.The Go team solved it without adding any language feature, which I would appreciate.
The reason I would be ok with explicit err is to follow the
declare before useconvention.handle errlooks like you’re giving the error a name, and then you can do anything with theerr.IMHO, I would just make
handledo one thing and do it well.@mvndaai Exactly. That’s also the reason why
trycouldn’t be trivially generalized to non-errortypes. It’s hard to improve over the simplicity and conciseness of anifstatement for general use.@mvndaai Although I think the Go team is unlikely to revisit error handling anytime soon, I do like your proposal and unless there are side-effects I do not yet see I would be happy to see it included in a future version Go. #fwiw
I updated the proposal to make
errexplicit. The changed was based on feedback and this quote from the Contracts/generics proposal.@mvndaai Explicit err helps. And I’m thinking of a type switch on errors.
The
()inhandle (err error)is unnecessary because theifstatement doesn’t need that too. Theoncould be something else.And I’ve got some rules. Don’t add more than one keyword. Solve error handling problem, not
err != nil. Keep consistency. Backward compatibility. etc.I want error handling be more powerful and enable us to write more robust code. Graceful error handling is just harder than anyone thinks.
@urandom that is on purpose. The proposal says:
Another way to do this would define handle blocks as
handle (err error) {}, but I thought since this could be used in nearly every third line it would be nice to be more concise.If handle blocks define variables it cold probably be overloaded to handle
okfunctions as well.Edit: Proposal changed to no longer have an implicit
err