Skip to content

proposal: spec: simplify error handling - error passing with "pass" #37141

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

Open
xbit opened this issue Feb 8, 2020 · 51 comments
Open

proposal: spec: simplify error handling - error passing with "pass" #37141

xbit opened this issue Feb 8, 2020 · 51 comments
Labels
error-handling Language & library change proposals that are about error handling. LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal Proposal-FinalCommentPeriod
Milestone

Comments

@xbit
Copy link

xbit commented Feb 8, 2020

Introduction

For the most part, I like the simplicity of error handling in Go, but I would very much like a less verbose way to pass errors.

Passing errors should still be explicit and simple enough to understand. This proposal builds on top of the many existing proposals and suggestions, and attempts to fix the problems in the proposed try built-in see design doc. Please read the original proposal if you haven't already some of the questions may already be answered there.

Would you consider yourself a novice, intermediate, or experienced Go programmer?

  • I have been using Go almost exclusively for the past two years, and I have been a developer for the past 10 years.

What other languages do you have experience with?

  • C, Java, C#, Python, Javascript, Typescript, Kotlin, Prolog, Rust, PHP ... and others that I don't remember.

Would this change make Go easier or harder to learn, and why?

  • It may make Go slightly harder to learn because its an additional keyword.

Who does this proposal help, and why?

  • This proposal helps all Go developers in their daily use of the language because it makes error handling less verbose.

Proposal

What is the proposed change?

Adding a new keyword pass.

pass expr

Pass statements may only be used inside a function with at least one result parameter where the last result is of type error.

Pass statements take an expression. The expression can be a simple error value. It can also be a function
or a method call that returns an error.

Calling pass with any other type or a different context will result in a compile-time error.

If the result of the expression is nil, pass will not perform any action, and execution continues.

If the result of the expression != nil, pass will return from the function.

  • For unnamed result parameters, pass assumes their default "zero" values.
  • For named result parameters, pass will return the value they already have.

This works similar to the proposed try built-in.

Is this change backward compatible?

Yes.

Show example code before and after the change.

A simple example:

before:

f, err := os.Open(filename)
if err != nil {
       return ..., err
}
defer f.Close()

after:

f, err := os.Open(filename)
pass err
defer f.Close()

In the example above, if the value of err is nil. it is equivalent to calling pass in the following way (does not perform any action):

pass nil

Consequently, the following is expected to work with pass (passing a new error)

pass errors.New("some error")

Since pass accepts an expression that must be evaluated to an error . We can create handlers without any additional changes to the language.

For example, the following errors.Wrap() function works with pass.

// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func Wrap(err error, message string) error {
    if err == nil {
    	    return nil
    }
   err = &withMessage{
	    cause: err,
	    msg:   message,
    }
    return &withStack{
	    err,
	    callers(),
    }
}

Because Wrap returns nil when err == nil, you can use it to wrap errors:

f, err := os.Open(filename)
pass errors.Wrap(err, "couldn't open file")

the following does not perform any action (because Wrap will return nil):

 pass errors.Wrap(nil, "this pass will not return")

You can define any function that takes an error to add logging, context, stack traces ... etc.

func Foo(err Error) error {
    if err == nil {
         return nil
    }      

    // wrap the error or return a new error
}

To use it

 f, err := os.Open(filename)
 pass Foo(err)

pass is designed specifically for passing errors and nothing else.

Other examples:

(Example updated to better reflect different usages of pass)

Here's an example in practice. Code from codehost.newCodeRepo() (found by searching for err != nil - comments removed)

This example shows when it's possible to use pass, and how it may look like in the real world.

before:

func newGitRepo(remote string, localOK bool) (Repo, error) {
	r := &gitRepo{remote: remote}
	if strings.Contains(remote, "://") {
		var err error
		r.dir, r.mu.Path, err = WorkDir(gitWorkDirType, r.remote)
		if err != nil {
			return nil, err
		}

		unlock, err := r.mu.Lock()
		if err != nil {
			return nil, err
		}
		defer unlock()

		if _, err := os.Stat(filepath.Join(r.dir, "objects")); err != nil {
			if _, err := Run(r.dir, "git", "init", "--bare"); err != nil {
				os.RemoveAll(r.dir)
				return nil, err
			}
	
			if _, err := Run(r.dir, "git", "remote", "add", "origin", "--", r.remote); err != nil {
				os.RemoveAll(r.dir)
				return nil, err
			}
		}
		r.remoteURL = r.remote
		r.remote = "origin"
	} else {
		if strings.Contains(remote, ":") {
			return nil, fmt.Errorf("git remote cannot use host:path syntax")
		}
		if !localOK {
			return nil, fmt.Errorf("git remote must not be local directory")
		}
		r.local = true
		info, err := os.Stat(remote)
		if err != nil {
			return nil, err
		}
		if !info.IsDir() {
			return nil, fmt.Errorf("%s exists but is not a directory", remote)
		}
		r.dir = remote
		r.mu.Path = r.dir + ".lock"
	}
	return r, nil
}

after:

func newGitRepo(remote string, localOK bool) (Repo, error) {
	r := &gitRepo{remote: remote}
	if strings.Contains(remote, "://") {	
		var err error
		r.dir, r.mu.Path, err = WorkDir(gitWorkDirType, r.remote)
		pass err

		unlock, err := r.mu.Lock()
		pass err
		defer unlock()

		if _, err := os.Stat(filepath.Join(r.dir, "objects")); err != nil {
			if _, err := Run(r.dir, "git", "init", "--bare"); err != nil {
				os.RemoveAll(r.dir)
				pass err
			}
	
			if _, err := Run(r.dir, "git", "remote", "add", "origin", "--", r.remote); err != nil {
				os.RemoveAll(r.dir)
				pass err
			}
		}
		
		r.remoteURL = r.remote
		r.remote = "origin"
	} else {
		if strings.Contains(remote, ":") {
			pass fmt.Errorf("git remote cannot use host:path syntax")
		}
		if !localOK {
			pass fmt.Errorf("git remote must not be local directory")
		}
		r.local = true
		info, err := os.Stat(remote)
		pass err
		
		if !info.IsDir() {
			pass fmt.Errorf("%s exists but is not a directory", remote)
		}
		r.dir = remote
		r.mu.Path = r.dir + ".lock"
	}
	return r, nil
}

What is the cost of this proposal? (Every language change has a cost).

  • How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?

Because this is a new keyword, I'm not sure how much some of these tools would be affected,
but they shouldn't need any significant changes.

  • What is the compile time cost?

Compile time cost may be affected because the compiler may need to
perform additional optimizations to function or method calls used with pass.

  • What is the run time cost?

This depends on the implementation. Simple expressions like this:

pass err

should have equivalent runtime cost to the current err != nil.

However, function or method calls will add run time cost and this will largely depend on the implementation.

Can you describe a possible implementation?

For the simple case, the compiler may be able to expand pass statements

pass err

to

if err != nil {
    return result paramters ...
}

For function or method calls there maybe a better way to do it
possibly inline these calls for common cases?

How would the language spec change?

The new keyword pass must be added to the language spec with a more formal definition.

Orthogonality: how does this change interact or overlap with existing features?

In some cases, pass may overlap with a return statement.

func someFunc() error {
    if cond {
        pass errors.New("some error") 
    }  
    ...   

instead of

func someFunc() error {
    if cond {
        return errors.New("some error") 
    }
    ...  

It may not be clear which keyword should be used here.
pass is only useful in these cases when there is multiple result parameters.

func someFunc() (int, int, error) {
    if cond {
        pass errors.New("some error") 
    }
    ...  

instead of

func someFunc() (int, int, error) {
    if cond {
        return 0, 0, errors.New("some error") 
    }  
    ...

(Edited to make example more clear)

Is the goal of this change a performance improvement?

No

Does this affect error handling?

Yes, here is the following advantages of pass and how it may differ from other proposals.

Advantages to using pass

  • pass is still explicit.
  • Supports wrapping errors and using simple handlers without new keywords such handle ... etc.
  • it makes it easier to scan code. Avoids confusing err == nil with err != nil. Since pass only returns when error is not nil in all cases.
  • should work fairly easily with existing error handling libraries.
  • should work with breakpoints
  • much less verbose.
  • Fairly simple to understand.

Is this about generics?

No

Edit: Updated to follow the template

@gopherbot gopherbot added this to the Proposal milestone Feb 8, 2020
@ianlancetaylor ianlancetaylor changed the title proposal: Go 2 simplify error handling - error passing with "pass" proposal: Go 2: simplify error handling - error passing with "pass" Feb 8, 2020
@ianlancetaylor ianlancetaylor added v2 An incompatible library change LanguageChange Suggested changes to the Go language labels Feb 8, 2020
@ianlancetaylor
Copy link
Member

Seems slightly unfortunate that we always have to call the errors.Wrap function even when there is no error.

@ianlancetaylor
Copy link
Member

For language change proposals, please fill out the template at https://go.googlesource.com/proposal/+/refs/heads/master/go2-language-changes.md .

When you are done, please reply to the issue with @gopherbot please remove label WaitingForInfo.

Thanks!

@gopherbot gopherbot added the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Feb 8, 2020
@ianlancetaylor ianlancetaylor added the error-handling Language & library change proposals that are about error handling. label Feb 8, 2020
@xbit
Copy link
Author

xbit commented Feb 9, 2020

@gopherbot please remove label WaitingForInfo

@gopherbot gopherbot removed the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Feb 9, 2020
@xbit
Copy link
Author

xbit commented Feb 9, 2020

The idea would be that as soon as err is non-nil, it's automatically returned.
This could probably be leveraged by more than just errors.
That being said, (err error pass) feels awfully long - but in my opinion, it would definitely be an improvement over having to type pass err at every step of the way.

Just my 2 cents

My concern is that it would make it less explicit and maybe a little more difficult to understand.
Calling pass is still easy. Imagine having a function with multiple return values. pass will make it much simpler anyway.

Also, Go allows separating statements with semicolons so you can technically do it in one line but not sure if it is something that would be preferred.

n, err := f.Write(data) ; pass err

if n < len(data) {
 return io.ErrShortWrite
}
return f.Close()

@MaxSem
Copy link

MaxSem commented Feb 9, 2020

Why not go one step further and do something like:

result, pass err := someFunc()

@yiyus
Copy link

yiyus commented Feb 9, 2020

Your version of WriteFile is wrong. While the original function will always close the file, your version will return if there is an error in f.Write or n<len(data) without closing the file. You could use defer, but then you will miss the error returned by f.Close. At the end, I think you would only be able to replace thee first return, so this may not be the most compelling example.

@xbit
Copy link
Author

xbit commented Feb 9, 2020

@yiyus good catch! I updated the example to show something a bit more involved, and when pass can realistically be used.

@ianlancetaylor
Copy link
Member

It seems that pass v requires v to have type error and is shorthand for

    if v != nil {
        return ..., v // here ... is the zero value for the other result parameters
    }

It's unfortunate that error wrapping requires calling the function even when the error is nil, but on the other hand it would not be too hard for the compiler to check whether the wrapper function starts with

    if err == nil {
        return nil
    }

and pull those lines into caller without inlining the whole function.

The name pass is certainly used as a variable name in existing code (e.g., https://golang.org/src/crypto/sha512/sha512_test.go#L664), and that code would break.

Also the name pass doesn't seem to really describe the operation here. The word "pass" often means to pass over something, but in this case we are--passing it up to the caller? Passing over the rest of the function? I'm not sure.

One possibility would be to use return? or return if instead of pass.

    return? err
    return if err

As you know, there has been a lot of discussion about error handling in Go. Is this language change big enough for the problem? It saves about six tokens and two lines. Is that worth the cost of a language change?

@rcoreilly
Copy link

I really like the return if err version of this idea: it directly summarizes the three lines of code that it replaces, and doesn't introduce any new keywords, logic or anything else other than addressing the one consistent sore point with the existing situation: it takes a lot of space and eyeball friction for such a high-frequency expression. Also, I've definitely mistyped err == nil instead of err != nil several times without noticing, so avoiding the need for that is a bonus.

You've already tried to change the language to fix this problem, so it seems that there is a general agreement that it is worth fixing, and this is just about as minimal, yet impactful, a change as could be made. If it survives the flamethrowers, it would probably build a good feeling in the community that there really is a willingness to fix the few existing sore points in the language..

@proyb6
Copy link

proyb6 commented Feb 26, 2020

The other possibility we could shorten with guard keyword because in most case if err != nil can be error prone for fast typing and @rcoreilly have the same issue. With guard provides less typing and encourage developers to handle the "error" instead of verifying if the error is not nil.

Return error (type errors) if not nil:
guard err

Return true (boolean) if the error match:
guard err == “code”

To return custom error (multiple variables):

guard err {
    return nil, err
}

@xbit
Copy link
Author

xbit commented Feb 27, 2020

The name pass is certainly used as a variable name in existing code (e.g., https://golang.org/src/crypto/sha512/sha512_test.go#L664), and that code would break.

It should be trivial for a simple tool to fix these usages (maybe go fix can deal with that?). It is reasonable to assume that for a new major version, you need to at least run a small command to make your code compatible.

Also the name pass doesn't seem to really describe the operation here. The word "pass" often means to pass over something, but in this case we are--passing it up to the caller? Passing over the rest of the function? I'm not sure.

One possibility would be to use return? or return if instead of pass.

    return? err
    return if err

pass means passing the error up to the caller. If you think about it, it is slightly similar to throw in other languages but more polite because it deals with error values instead of exceptions.

return if and return? are also worth considering. But I think another usage of pass will naturally lend itself at least because it is possible.

Instead of filling zero values for the result parameters, it is possible to do this:

func someFunc() (int, int, error) {
    if n > N {
        return? errors.New("n cannot be greater than N") 
    }
    
    ...  

In this case, pass may sound slightly better.

func someFunc() (int, int, error) {
    if n > N {
        pass errors.New("n cannot be greater than N") 
    }

    ...  

Normal usage:

func someFunc() (int, int, error) {
    if n > N {
        return 0, 0, errors.New("n cannot be greater than N") 
    }

    ...  

but I think I like return? mostly because pass may overlap with return in some cases.

As you know, there has been a lot of discussion about error handling in Go. Is this language change big enough for the problem? It saves about six tokens and two lines. Is that worth the cost of a language change?

If developers only had to write if err != nil { ... } once then, it may not be worth it, but if that was the case there wouldn't be many error handling proposals to begin with. Six tokens and two lines add up. It is not ergonomic.

This proposal should fix the most common case. If you want to handle the error (not pass it), i like the current method. Errors are values, you can check for it with an if statement and do something. Adding something like check and handle may make the code only slightly cleaner in some cases but can also add more complexity.

This example looks pretty good as well if you prefer return? instead of pass:
It is also a good representation of the harder cases.

func CopyFile(src, dst string) error {
	r, err := os.Open(src)
	return? fmt.Errorcf(err, "copy %s %s: %v", src, dst, err)
	defer r.Close()

	w, err := os.Create(dst)
	return? fmt.Errorcf(err, "copy %s %s: %v", src, dst, err)

	if _, err := io.Copy(w, r); err != nil {
		w.Close()
		os.Remove(dst)
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	if err := w.Close(); err != nil {
		os.Remove(dst)
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}
	return nil	
}

It is much more ergonomic in my opinion than the current way (btw the example above takes only one extra line compared to check and handle example https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md).

@ianlancetaylor
Copy link
Member

It is reasonable to assume that for a new major version, you need to at least run a small command to make your code compatible.

We can certainly do this if we must. But it's a cost. We shouldn't just ignore that cost.

pass means passing the error up to the caller.

I guess. That doesn't seem like a natural meaning of "pass" to me, though. The first meaning that jumps to mind for me is to pass over something, meaning to ignore it. Any other opinions on this point?

I don't dispute that pass is more ergonomic. The thing is, we only get to change this once. We can't iterate on error handling. Is this improvement ergonomic enough?

@ianlancetaylor
Copy link
Member

I see now that a problem with return? or return if is that they would only list a single result, whereas the normal return statement of course lists all results (or none, if the results are named).

@rcoreilly
Copy link

pass means passing the error up to the caller.

I guess. That doesn't seem like a natural meaning of "pass" to me, though. The first meaning that jumps to mind for me is to pass over something, meaning to ignore it. Any other opinions on this point?

it is returning the error up to the caller, and given that we already have a keyword that means return it seems like it makes sense to use that, right?

Given that the language already has two forms of return logic (named vs. unnamed), adding a third seems reasonable. The named form already does some "magic" by not directly listing the values returned. This proposed form just adds an additional, logical case that says if you're returning without achieving the mission of the function, return zero values.

return? maybe seems too "ternary" for the Go language? return if seems like a natural way to say "conditional return" using the existing language elements..

@xbit
Copy link
Author

xbit commented Feb 27, 2020

I don't dispute that pass is more ergonomic. The thing is, we only get to change this once. We can't iterate on error handling. Is this improvement ergonomic enough?

What I like about Go is that it often picks the simplest approach to solve a problem. The proposed change is consistent with that. It is a minimal change, but very impactful. It is ergonomic. It reduces boilerplate code and helps you focus on the critical parts of a function.

If you choose to go with return? or return if instead of pass, "conditional return" does sound like a good name for it. For me return? looks nicer than return if after trying it in some examples.

@DeedleFake
Copy link

DeedleFake commented Feb 28, 2020

Seems slightly unfortunate that we always have to call the errors.Wrap function even when there is no error.

This is a concern, especially for uses of fmt.Errorf() with %w. If a lot of errors in a function need wrapping, that's a lot of pointless overhead to parse the formatting string, run stuff through reflection, and allocate new stuff just to throw it away 90% of the time.

Maybe as an alternative, an optional second argument could be given and, if it is, than the the first argument is only used for the nil check. For example,

// If err is not nil, execute and return the second argument.
return? err, fmt.Errorf("some extra information: %w", err)

// Alternative: Do a simple boolean check on the first argument. That
// makes this more cleanly just shorthand for an if statement.
return? err != nil, fmt.Errorf("some extra information: %w", err)

The downside to this, though, is that with just a ? to differentiate, this could be a bit awkward to read at a glance. The alternative above in particular might work better with the return if syntax instead.

Edit: This is assuming that fmt.Errorf() returned nil if the %w target is nil, which I don't think that it does at the moment.

@xbit
Copy link
Author

xbit commented Feb 28, 2020

@DeedleFake I didn't use fmt.Errorf() in the example with return?. I used fmt.Errorcf() which takes an error as the first argument to check for nil. This function doesn't actually exist but it is just an example.

@TwiN
Copy link

TwiN commented Feb 28, 2020

The more I think about it, the more I like the way errors are currently handled in Go.

IMO, return? sounds great in theory, but in practice, I have a feeling it'll make reading the code harder.

Not just that, but ignoring other use cases, for error handling, all this is doing is removing the need for writing if err != nil { and }.

Let's say that this is sufficient in term of error handling and we decide to go with return? or return if, then we're met with the fact that since we're using something as generic as the keyword return, return? will need to support returning multiple values - not just one.

To do that, return? would need to have an extra argument - a condition, which like I said earlier, will most likely make the code harder to read.

Unlike what @DeedleFake mentioned, I don't think the default behavior should be checking if the first argument is nil, because if return? is to work with values other than error, then what happens with values that cannot be nil (i.e. string)?

This leaves us with the following:

something, err := getSomething()
return? err != nil, nil, err 
return something, nil

I think that this is a downgrade from the existing:

something, err := getSomething()
if err != nil {
    return nil, err 
}
return something, nil

Even though there's more LoC, at least there's no confusion, and the number of returned values match with the function signature.

Also, the examples used for the return? condition all have a conveniently small conditions, but take something like this as example:

func ValidatePassword(password string) (bool, error) {
	if len(password) < MinPasswordLength {
		return false, fmt.Errorf("password must have at least %d characters", MinPasswordLength)
	}
	if len(password) > MaxPasswordLength {
		return false, fmt.Errorf("password must have less than %d characters", MaxPasswordLength)
	}
	return true, nil
}

With return?, we'd have this instead:

func ValidatePassword(password string) (ok bool, err error) {
	return? len(password) < MinPasswordLength, false, fmt.Errorf("password must have at least %d characters", MinPasswordLength)
	return? len(password) > MaxPasswordLength, false, fmt.Errorf("password must have less than %d characters", MaxPasswordLength)
	return true, nil
}

It looks nice, but the lines are quite a bit longer - and there's just two values being returned.

Personally, with named parameters, it'd look even better:

func ValidatePassword(password string) (ok bool, err error) {
	if len(password) < MinPasswordLength {
		err = fmt.Errorf("password must have at least %d characters", MinPasswordLength)
	} else if len(password) > MaxPasswordLength {
		err = fmt.Errorf("password must have less than %d characters", MaxPasswordLength)
	} else {
		ok = true
	}
	return
}

But I digress.

That said, I think the suggestion for pass has its merits, but more importantly, it's targeted at error handling, whereas return? has a wider implications that also happens to be something that can be leveraged for error handling.

As @ianlancetaylor mentioned, I also think that the word pass might be inappropriate for this use case. pass does not convey "pass this value to the caller if the value passed is not nil". assert or check would make more sense for me, but even these two don't feel explicit enough to me.

@xbit
Copy link
Author

xbit commented Feb 28, 2020

@TwinProduction if return? is preferred instead of pass. Even if we wanted to support optionally adding return values for return?. It should still match the result parameters. There is no need for an additional condition, and it will still work the same way.

func someFunc() (int, error) {
    f, err := os.Open(filename)
    return? 0, err
   
    // do other things

the conditional return will only check if the error value != nil (the proposal already specifies that the error type must be the last result parameter)

@ianlancetaylor
Copy link
Member

I haven't seen anybody try to address this point:

It saves about six tokens and two lines. Is that worth the cost of a language change?

As far as the keyword goes, what about check?

One of the arguments against the try proposal (#32437) was that it introduced another kind of flow of control. This same consideration arises with this proposal.

It's a little odd that in an expression like

    pass errors.Wrap(err, "x")

the reader has to understand that errors.Wrap will always be called, but that the pass (or whatever) is basically keyed on whether the original err value is nil (but expressed as whether errors.Wrap returns nil). This is definitely doable, but is it confusing? Especially to people new to Go?

@OneOfOne
Copy link
Contributor

@ianlancetaylor since it's a keyword, can't we make it lazy evaluate the call?

func xyz() (int, err) {
	x, err := fn()
	pass errors.Wrap(err, "xyz")
	
	if x != 0{
		return x * x * x, nil
	}

	return 1, nil
}

// would be the same as:

func xyz() (int, err) {
	x, err := fn()
	if err != nil {
		return 0, errors.Wrap(err, "xyz")
	}

	if x != 0{
		return x * x * x, nil
	}

	return 1, nil
}

@switchupcb
Copy link

There is another variation that can be used for explicitness; I can create it once my keyboard is no longer broken.

@switchupcb
Copy link

Otherwise, this pass proposal is our last hope.

@switchupcb

This comment was marked as off-topic.

@fumin
Copy link

fumin commented Jun 7, 2022

Honestly, I'm leaning more and more towards some kind of limited macro system to deal with this.

Building on @DeedleFake 's idea, I think the key is how to limit the macro system.
It seems that the reason why error handling is hard is because there's no way to shortcut return, except for macros.
Therefore, perhaps one way to "limit" the macro system would be to support only returns.

One benefit of going with the macro the approach, is that everyone can have their favorite word, be it pass, return if etc.
This sidesteps the language keyword issue.

Implementation wise, perhaps //go:generate is something we can tap into.

Rob Pike argued in #19623 that arbitrary precision integers might be the reason for a Go 2.0.
I would wager that a macro system would be another equally powerful reason for a Go 2.0.

@beoran
Copy link

beoran commented Jun 23, 2022

@fumin, the current mood of Go seems to be against macros. It might be a good solution in this case but the compiler should not become Turing complete, and macros have a high risk of being inadvertently Turing complete.

No matter what the keyword, or perhaps, built in function is named, I think the most general use is a kind of return statement that returns only of all it's returned expressions are not nil. So return? or return if, seem like good names for that.

As to the question if such a shorthand is worth while, I think it is because as @rcoreilly states, the frequency of use is the key here. My experience in Ruby which has similar expression ifs is that they are often used, very useful, and easy to read.

@mdcfrancis
Copy link

I have to assume this has been proposed before but I did not see it.

Why not take a lesson from if and for and define a version of return that takes a second optional arg, a boolean condition? Return is already special in that there exists a zero-argument form with named returns.

For example; we often write code that passes the error up the stack:

if x, err := foo(); err != nil { 
     return nil, err 
}
// note x and err not available here 

if we allow return to be used as

return nil, err ; err != nil

then you have a short form that short circuits and returns from the function. It doesn't change the semantics of the return expression to the left of the condition and defers etc work as before. You can mentally read this form of return as return if .

There is added potential to write expressions such as

return x, err := foo() ; err != nil 
// note x and err available here

though this last form may be a step too far.

zero argument returns continue to work, assuming the use of named arguments.

return ; err != nil 

Net introduces no new symbols or keywords and is consistent with for and if

@Wulfheart
Copy link

@mdcfrancis I am afraid that it will make the code much less unreadable.
So this

f, err := os.Open(filename)
if err != nil {
       return ..., err
}
defer f.Close()

would become that

f, err := os.Open(filename)
return ..., err; err != nil
defer f.Close()

Now imagine you would like to handle it in multiple cases. How are you going to distinguish between a "legit" return or just a simple error return when looking at the left edge of the code?

@rcoreilly
Copy link

To my eyes, scanning for the return keyword on the left edge tells me all the places where the function returns, and then looking to the right clarifies the conditions. I think it may indeed be easier to quickly see the flow, especially given the primary benefit of just removing lots of extra "clutter".

Under this proposal, the remaining if statements would then become actual "content" logic instead of "distracting" error-handling boilerplate, so it would also be easier to quickly see the actual function of the function..

Overall, I really like this latest proposal from @mdcfrancis!

@gregwebs
Copy link

I created a new proposal that is the same as this one but adds a second parameter for error handling so that error handling can be first class.

This proposal has the right foundation, but I believe it is a mistake to not optimize for error handling: it encourage Go programs to not annotate their errors. errors.Wrap is a creative workaround, but still leaves error handling as a second class: it obscures that err is really what is being checked and it requires reading code and looking at the calling function to ensure that it checks for nil.

There is a lot of debate about what exact keyword/syntax to use on this proposal. I would prefer to have that discussion separately and focus on getting a proposal tentatively semantically accepted with the provision that the keyword can still be changed to whatever the community decides is optimal.

@xiaokentrl

This comment was marked as spam.

@xiaokentrl

This comment was marked as spam.

@chad-bekmezian-snap
Copy link

It is reasonable to assume that for a new major version, you need to at least run a small command to make your code compatible.

We can certainly do this if we must. But it's a cost. We shouldn't just ignore that cost.

pass means passing the error up to the caller.

I guess. That doesn't seem like a natural meaning of "pass" to me, though. The first meaning that jumps to mind for me is to pass over something, meaning to ignore it. Any other opinions on this point?

I don't dispute that pass is more ergonomic. The thing is, we only get to change this once. We can't iterate on error handling. Is this improvement ergonomic enough?

What if we used surrender? It’s a bit longer, but seems to convey what is happening a bit more accurately.

@ianlancetaylor ianlancetaylor changed the title proposal: Go 2: simplify error handling - error passing with "pass" proposal: spec: simplify error handling - error passing with "pass" Aug 6, 2024
@ianlancetaylor ianlancetaylor added LanguageChangeReview Discussed by language change review committee and removed v2 An incompatible library change labels Aug 6, 2024
@aarzilli
Copy link
Contributor

aarzilli commented Feb 20, 2025

I think of all the error handling proposals this is the one I like the most. Altough this would be better with pass being a new builtin instead of a keyword:

pass(X) is equvalent to:
if x := X; x != nil {
     return ..., x
}

and

pass(X, Y) is equivalent to:
if X != nil {
    return ..., Y
}

Predeclare identifiers have been added to the language several times to the language in recent years, and have caused little disruption.

@ianlancetaylor
Copy link
Member

This is a good proposal, but it doesn't seem to be enough of an improvement to be worth the language change. It introduces a new keyword, which is not impossible but is problematic. People still have to give the error result a name in order to test it.

This converts

    f, err := os.Open("file")
    if err != nil {
        return fmt.Errorf("no file: %v", err)
    }

to

    f, err := os.Open("file")
    pass errors.Wrap(err, "no file")

This saves two lines and six tokens. That's pretty good but we do have to name, and potentially shadow, err.

The change in control flow is somewhat obscure. When using errors.Wrap, whether the function returns is controlled by the argument to errors.Wrap. This is consistent but can be somewhat obscure. It will be particularly obscure to people new to the language.

For these reasons this is a likely decline. Leaving open for four weeks for final comments.

-- for @golang/proposal-review

@xyzdev-cell
Copy link

I just want to know, when there are multiple return values, such as func() (someStruct, yetVeryLargeStruct, error) {}, if I use pass err or return if err, will the compiler automatically construct the structs that need to be returned (which are actually rarely used)? This doesn't seem like the style of the Go compiler.

@aarzilli
Copy link
Contributor

will the compiler automatically construct the structs that need to be returned (which are actually rarely used)? This doesn't seem like the style of the Go compiler.

The Go compiler initializes variables to their zero value in a lot of places, for example when creating slices make([]T, n), when declaring variables var x structType, or with named returns func () (x int) { return }.

@vtopc
Copy link

vtopc commented Mar 14, 2025

The Go compiler initializes variables to their zero value in a lot of places, for example when creating slices make([]T, n)

It's not. Zero value for slice - is nil.

@aarzilli
Copy link
Contributor

It's not. Zero value for slice - is nil.

I meant that the contents of the slice are initialized to the zero value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
error-handling Language & library change proposals that are about error handling. LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal Proposal-FinalCommentPeriod
Projects
None yet
Development

No branches or pull requests