go: proposal: Go 2: orelse for streamlined error handling

as per @ianlancetaylor: "Please let's not have a discussion about error handling in general on this issue. Please only discuss this specific proposal. Thanks."

This proposal is not about avoiding typing few keystrokes or shirking responsibilities for error handling or about making error handling in Go like any other language. It simply aims to use the compiler to reduce the mental workload of reading/reviewing error-heavy code. Error handling with this approach will remain as explicit and as boring as ever!

Author background

  • Medical doctor/Professor who has been using Go since 2013 in several projects, and has experience with several other languages (c, Pascal, Javascript).

Proposal

I propose a new keyword orelse that is:

  • legal only, but not mandatory, following an assignment to a variable that satisfies the error interface, and
  • triggers the execution of an error-handling code block when the error variable is not nil. Other than being automatically triggered, there is nothing new or special about the triggered block.

Example

The example code in Russ Cox’s paper[1] will look like this:

func CopyFile(src, dst string) error {
	//one line orelse 
	r, err := os.Open(src) orelse return fmt.Errorf("copy %s %s: %v", src, dst, err)
	defer r.Close()
	
	// orelse does not open a new scope
	w, err := os.Create(dst) orelse return fmt.Errorf("copy %s %s: %v", src, dst, err)
	
	// multi-line orelse block
	err := io.Copy(w, r) orelse {
		w.Close()
		os.Remove(dst)
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

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

Rationale

The verbosity of Go’s standard error handling approach is now a top concern for Go developers responding to the last annual survey. A more ergonomic approach should encourage adopting best practices of error handling and make code easier to read and maintain.

Compared to the current approach, I believe the proposed approach is:

  • significantly less verbose, e.g., the ratio of error-handling to program logic lines in the above program is 5:5 compared to 13:5 in the original sample,
  • as explicit; both the error-handling code and the potential for change in program flow are at least as clear,
  • no less flexible since it permits ignoring the error, returning it as is or wrapping it.

Because orelse is not used for any other purpose, it would be easy for reviewers and linters to spot lack of error handling. And because it is semantically similar to an else block, it should be easy to learn and understand.

Additional advantages include:

  • it builds on recent improvements in the standard errors package such as wrapping and unwrapping errors, e.g.,
_, err := io.ReadAll(r) orelse return errors.Wrap(err, "read failed")

  • it works well with named/bare returns, e.g.,
func returnObjOrErr() (obj Obj, err error) {
  obj, err := createObj() orelse return  //returns nil and err
}	
  • orelse does not open a new scope when it is not desired making it more ergonomic and reducing the risk for variable shadowing as in this example of a widely used idiom:
	if w, err:= os.Create(dst); err!= nil {} // new scope is opened preventing 
            // subsequent use of w and possibly shadowing a func-level err variable. 
  • it is still “Go-like”; each error is still handled individually although it should be easier to call a closure or a method to further deduplicate error handling. In this sense, it is similar to the try/handle approach without requiring an extra new keyword or inserting try in the middle of expressions.
func CopyFile(src, dst string) error {
    copyErr:= func(err error) {
		// do other stuff: log the error, set flags,
              //  format a nice error message etc
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }   
    r, err := os.Open(src) orelse return copyErr(err) 
  
    w, err := os.Create(dst);  orelse return copyErr(err)
    
   ... etc
}
  • it is backward compatible and does not prohibit those who prefer to use the current approach from continuing to use it.

  • it can handle the very rare situation when a function returns more than one error. The oresle block is invoked if any error is not nil.

Costs and risks

  • one extra keyword, although a familiar one and most previous proposals introduce at least one keyword or punctuation.
  • some proponents of the current approach may fear that it might encourage not handling errors. Whereas it is hard to be certain, I do suspect that having an ergonomic approach and a keyword dedicated to error handling will encourage proper error handling and make it easy to include correct examples of it in tutorials and code samples instead of skipping it as is often the case currently.
  • I do not foresee a significant impact on compile or runtime performance.

[1] https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 17
  • Comments: 24 (18 by maintainers)

Most upvoted comments

I’m not arguing for or against this proposal, but it could simply use ‘else’ instead of introducing a new keyword. It should be unambiguous to parse in that position.

After read the proposal, I’d like to introduce a derivative idea for consideration. The aim is to provide a more concise way this, while remaining explicit and consistent with Go’s established idioms.

f, err := os.Open("file.txt"); else { return err }
data, err := ioutil.ReadAll(f); else { return err }
// continue processing data

I don’t want to open similar proposal for this one. Just an idea.

Does orelse allow any statement? Or just returns?

Note that the condition for the orelse statement is implicit here (err!= nil)

I’m not too sure about that.

( the advantage of the current error handling is that it uses the most basic of all control-flow structure, not sure that’s something that can be replaced easily)

in fact it simplifies error handling and learning and teaching it

You can say a lot about the current way of error handling (boring, repetitive, etc.), but not that it’s difficult to learn or teach. In fact it’s so simplistic that even the noobiest of programming noobs understands it immediately.

The solution space is very restricted… so it is not surprising that there will be similarities to previous proposals.

OK, but those previous proposals were rejected. We don’t want to repeatedly revisit past decisions. If we are to consider a proposal that is similar to previously rejected proposals, we want to see new information that explains why this proposal is different.

I think what happens with this issue is that the many people who want change are not as motivated as the “error handling must be painful camp”.

Introducing more ways of doing the same adds complexity to a language.

That is a generalization… often true. but not in this case. Complexity is not just about the count of keywords or ways of doing things… it is anything that unnecessarily add to the mental load of the reader. I provided above some concrete way of measuring it in this particular case: the ratio of the number error handling lines to program logic lines.

No… we have evidence from several surveys that it is a significant number of people.

The surveys indicate a significant number, but do not confirm whether it constitutes a majority. What we know is that no single error handling proposal has garnered a majority of support so far.

it does not complicate the language (in fact it simplifies error handling and learning and teaching it)

Introducing more ways of doing the same adds complexity to a language.

No… we have evidence from several surveys that it is a significant number of people. The important thing here is that the changes proposed do not affect those who prefer to use the current approach… it does not complicate the language (in fact it simplifies error handling and learning and teaching it) and it does not affect compiling or runtime performance so it is hard to see why the resistance.