Description
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