Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: Go 2: support "assign if nil" statement to tackle error handling boilerplate #21732

Closed
faiface opened this issue Sep 1, 2017 · 16 comments
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@faiface
Copy link

faiface commented Sep 1, 2017

Hello,

it seems to me that when people complain about Go's error handling, they mostly complain about the situations when it gets repetitive (otherwise I assume it's not a big problem), like this:

a, b, err := f1()
if err != nil {
    return nil, errors.Wrap(err, "failed")
}
c, err := f2(a)
if err != nil {
    return nil, errors.Wrap(err, "failed")
}
d, err := f3(b, c)
if err != nil {
    return nil, errors.Wrap(err, "failed")
}

Now, I usually don't encounter code like this, but I agree that this looks awful and is hard to prevent sometimes.

So I came up with a simple solution. Keep in mind, the solution is just an idea which might be iterated upon, or completely thrown away.

Assign if nil statement

Here's my solution.

In an assignment statement (= or :=), when any LHS operand is surrounded by parenthesis, the assignment will evaluate and assign if and only if all parenthesis-surrounded LHS operands are zero value (nil or an appropriate equivalent)

In case of := assignment, variables undeclared before are treated as zero valued.

What I mean is basically that this

a, b, (err) := f1()

gets translated to this

// assume a, b and err are declared
if err == nil {
    a, b, err = f1()
}

In general, something like this

a, b, (c), (d) := f()

gets translated to

if c == nil && d == nil {
    a, b, c, d = f()
}

How does this help?

With "assign if nil" statement, we can rewrite the original error handling chain like this:

a, b, (err) := f1()
c, (err) := f2(a)
d, (err) := f3(b, c)
if err != nil {
    return nil, errors.Wrap(err, "failed")
}

In the first line, err is undeclared and thus is handled as if it was nil and f1 executes. Next line only executes f2 and assigns the values if err is still nil, i.e. when the previous call finished without errors. The same holds for the third line.

In the last line, we nicely wrap the error with additional context information and return it.

Other uses

There are more uses of the "assign if nil" statement. For example, lazily initializing a map:

func (s *Struct) Method() {
    (s.m) = make(map[string]string) // this only gets executed if s.m is nil
}

Summary

I propose introducing an "assign if nil" statement. Proper formal specification is not done here, but is easy to imagine.

I think this construct solves most of the hassle with error handling in Go. An if err != nil once a while is perfectly fine, the only problem is when there is too many of them. I believe that's almost always possible to avoid using this construct.

Looking forward to your feedback!

Michal Štrba

@gopherbot gopherbot added this to the Proposal milestone Sep 1, 2017
@cznic
Copy link
Contributor

cznic commented Sep 1, 2017

In an assignment statement (= or :=), when any LHS operand is surrounded by parenthesis, ...

https://play.golang.org/p/LenMQLQyIU

@faiface
Copy link
Author

faiface commented Sep 1, 2017

@cznic Yeah, it conflicts with the current syntax. There are a few ways to get around that:

  1. Outer parentheses on LHS always mean "assign if nil". In the current language, they can always be removed anyway withou changing the meaning.
  2. Use another syntax for "assign if nil" such as a different kind of parentheses or something else.

@ianlancetaylor
Copy link
Contributor

As I mentioned over on #21161, I personally would prefer to see any change to error handling make it just as easy to wrap an individual error with more information as it does to simply move the error along.

@faiface
Copy link
Author

faiface commented Sep 1, 2017

@ianlancetaylor As you can see in the proposal, the error is wrapped with contextual information at the end.

d, (err) := f3(b, c)
if err != nil {
    return nil, errors.Wrap(err, "failed")
}

Functions that return errors should (and usually do) provide contextual informatiom themselves. Therefore you don't need to return "failed on pkg.Func" because that info is provided by the error returned from pkg.Func itself. That's why it's possible to assign the same contextual information to almost all returned errors and thus not deal with them individually, but group them as the proposal suggests.

When higher contextual information granularity is needed, one just falls back to the old style. But, I think that in such cases it doesn't feel like a boilerplate, because you're doing different things with each error.

@faiface
Copy link
Author

faiface commented Sep 2, 2017

It would be nice if someone gave this a Go2 label.

@cespare cespare added the v2 A language change or incompatible library change label Sep 2, 2017
@mrkaspa
Copy link

mrkaspa commented Sep 3, 2017

what if we add a ? in the error variable

d, err? := f3(b, c)

and this is like if we had this code

if err != nil {
    d, err := f3(b, c)
    return nil, errors.Wrap(err, "failed")
}

This solution es pretty similar to the one that rust uses for error handling https://m4rw3r.github.io/rust-questionmark-operator

@faiface
Copy link
Author

faiface commented Sep 3, 2017

@mrkaspa I like the question mark as a replacement for the parenthesis. So I would write this:

a, b, err? := f1()
c, err? := f2(a)
d, err? := f3(b, c)
if err != nil {
    return nil, errors.Wrap(err, "failed")
}

That's pretty nice, probably even nicer than my original proposal.

However, I don't think that automatically returning on ? is a good idea, like Rust does it.

@mrkaspa
Copy link

mrkaspa commented Sep 3, 2017

@faiface I think it is, because this is the problem that godevs have when the handle they need to do is to return the error, if they need to handle this in another way they can use only err and handle it with a custom if

@SCKelemen
Copy link
Contributor

I think the question mark is generally used with null/nil.
Java, C#, Swift, and other languages use the ? to denote optional/nullable/nilable variables, as well as for the null/nil coalesce operator.

@mrkaspa
Copy link

mrkaspa commented Oct 3, 2017

What about if when I do this:

a, b, err? := f1()

expands to this:

if err != nil {
    return nil, errors.Wrap(err, "failed")
}

and I can force it to panic with this:

a, b, err! := f1()

expands to this:

if err != nil {
   panic(errors.Wrap(err, "failed"))
}

This will keep backward compatibility, and will mend all the pain points of error handling in go

@mrkaspa
Copy link

mrkaspa commented Oct 3, 2017

@SCKelemen in those languages the ? goes on the type not in the variable

@faiface
Copy link
Author

faiface commented Oct 3, 2017

@mrkaspa
I really am against returning on the (...) or ? syntax. The reasons are:

  1. It breaks the flow and can be hard to read.
  2. Makes it hard to return customized error values.

Just to clarify, my proposal is to simply skip the assignments where variables marked with ? (or (...) in the original proposal) are not nil (or zero value in general).

@buchanae
Copy link
Contributor

Just to check my understanding, given this code:

var errF1 = errors.New("f1 error")

func f1() (string, error) {
  return "f1", errF1
}
func f3() {
  fmt.Println("f3")
}
// f2(), f4() purposely left out, they're implementations aren't important.

func run() error {
  _, (err) := f1()
  _, (err) := f2()
  f3() // f3() is always executed?
  _, (err) := f4()
  return err
}

run() would return errF1 and the code would print f3. Is that right?

@faiface
Copy link
Author

faiface commented Nov 15, 2017

@buchanae That's right!

@metakeule
Copy link

@faiface
I like the idea, but the syntax has to be worked on.
What I dislike is the fact that condition and result is not clearly understandable.

Since go does not have a ternary operator, what about this:

a, b :=  err ? f1()

which could obviously be extended to

a, b :=  err ? f1() : fallbackA, fallbackB

The docs would be as clear as:

"The ternary operator in go differs from that in C in that the condition is a single variable which is implicitly checked for being a zero value".

@ianlancetaylor
Copy link
Contributor

Issue #21161 is a general discussion of error handling changes to the languages. Closing this issue in favor of that one.

@golang golang locked and limited conversation to collaborators Mar 20, 2019
@bradfitz bradfitz added the error-handling Language & library change proposals that are about error handling. label Oct 29, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
error-handling Language & library change proposals that are about error handling. FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

12 participants