Repository: go
Issue: proposal: spec: simplify error handling - error passing with "pass"
Labels: LanguageChange,Proposal,error-handling,LanguageChangeReview

# 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](https://github.com/golang/go/issues/32437) see [design doc](https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md). 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()`](https://github.com/pkg/errors/blob/614d223910a179a466c1767a985424175c39b465/errors.go#L184) 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()`](https://github.com/golang/go/blob/3f51350c706c8ff663f12867bcfec98aa9fc46bf/src/cmd/go/internal/modfetch/codehost/git.go#L72) (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](https://github.com/golang/go/issues?utf8=%E2%9C%93&q=label%3Aerror-handling).


#### 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](https://go.googlesource.com/proposal/+/refs/heads/master/go2-language-changes.md)