Description
Go has suffered from seen a number of Go2 error handling proposals. I'm joining the chorus as I had the feeling that a comparatively less disruptive permutation of the discussion has not come up before. I'd be totally happy if this got closed right away, in that case sorry for wasting everybody's time.
Goals of this proposal:
- decrease verbosity, i.e. less typing and clearer structure of the non-error case
- full control over error handling, including annotating the error
- avoid hidden or non-local control flow magic
Before going to the details, I have considered:
- proposal: Go 2: block scoped error handling #33161 (block-scoped error handling) which attracted a comment for verbostity in proposal: Go 2: block scoped error handling #33161 (comment) but also was compared with Swift's
guard
in proposal: Go 2: block scoped error handling #33161 (comment). - Proposal: A built-in Go error check function, "try" #32437 (A built-in Go error check function, "try") which I personally feel introduces too much non-local error handling (using defer) and allows for hard to read nested calls of
try()
- proposal: Go 2: simplify error handling with try err == nil {} except {} #33387 (simplify error handling with try err == nil) which attracted this notable comment proposal: Go 2: simplify error handling with try err == nil {} except {} #33387 (comment) (unique per-statement error handling)
- Proposal: Go2: Drying up Error Handling in Go #36284 which partially failed due to Proposal: Go2: Drying up Error Handling in Go #36284 (comment) (fails to address the most common case of uniquely annotating errors in-situ.)
Proposal
This is an adapted version of #33161 (comment), I couldn't find an original proposal to this case (/cc @carlmjohnson).
In short, I'm proposing to introduce a try
keyword similar to Swift's guard
that will be implemented as an error-checking and error-handling specific alternative to the existing if
statement:
try
must me followed by an assignment expression where the last assignment parameter must be an error and can be omitted- if the error parameter is omitted, a non-nil error (and other zero value or initialised variables) will be returned alongside with it. This does not require the error to be named in the API
- if the
try
statement has a handler block, the block will be executed/ the error can be handled similar toif err != nil
and returned or not. This typically requires the err parameter to be named. - unlike the
if
statement, a special scoping rule allows the assigned target variables (right term?) to escape the block (I understand this is similar to swift). This includes the error variable if it is named.
As such, this proposal is similar to #33161 (comment), taking the comments regarding control flow from #33161 (comment) into account.
Syntactically, the two following statements are equivalent:
try foo := bar()
try foo := bar() {
return
}
It would look like
TryStmt = "try" AssignmentExpression [ Block ]
Examples
CopyFile
func CopyFile(src, dst string) error {
try r := os.Open(src) // returns the error
defer r.Close() // returns the error
try w := os.Create(dst) // returns the error
defer w.Close() // returns the error
try io.Copy(w, r) // returns the error
try w.Close() // returns the error
}
Update: Obviously this example needs an additional rule for error precedence during Updated the defer
(tbd).defer
Syntax. Using try
inside deferred functions follows the same rules as everywhere else.
Hex
func main() {
try hex, err := ioutil.ReadAll(os.Stdin) {
log.Fatal(err)
}
try data := parseHexdump(string(hex)) {
log.Fatal(err)
}
os.Stdout.Write(data)
}
Some thoughts
As @carlmjohnson said:
- I think if something like this is done, it should be called
guard
because that's a name used by another language. - Probably it's not enough better than `if to be adopted.