go: Proposal: Go2: Error checking and handling inspired on Switch/Case statements
Proposal: Go2: Error checking and handling inspired on Switch/Case statements
- Author: Gustavo Bittencourt
Summary
This proposal tries to address an aspect of Go programs which “have too much code checking errors and not enough code handling them” (Error Handling — Problem Overview). To solve this, the proposal is inspired by Switch/Case statements in Go.
Introduction
This proposal creates the new statement, handle, that defines a scope where one variable is checked and handled as soon there is assignment operation with that variable. That variable is called “handled variable”. For example, the following code is how Go handles errors:
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
}
y, err := strconv.Atoi(b)
if err != nil {
return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
}
fmt.Println("result:", x+y)
return nil
}
In this proposal, the function could be the following:
func printSum(a, b string) error {
var err error
handle err {
x, err := strconv.Atoi(a)
y, err := strconv.Atoi(b)
fmt.Println("result:", x + y)
case err != nil:
return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
}
return nil
}
The handle statement defines a handled variable (err in the above example) and a scope where this variable is handled. In the handle scope, each time the handled variable is in the left-hand side of an assignment operation, immediately after that assignment operation, it will be executed the checks defined in the case statements.
The case statements are evaluated top-to-bottom; the first one that is true, triggers the execution of the statements of the associated case; the other cases are skipped. If no case matches no one is executed. There is no “default” case.
Defining error handled-variable in the handle statement
The handled variable can be defined in the handle statement, so the code can be even shorter.
func printSum(a, b string) error {
handle err error { // <-- the variable 'err' is defined here
x, err := strconv.Atoi(a)
y, err := strconv.Atoi(b)
fmt.Println("result:", x + y)
case err != nil:
return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
}
return nil
}
Example: Error handling with defer statement
In Golang:
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return err
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return err
}
defer w.Close()
if _, err := io.Copy(w, r); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
}
In this proposal:
func CopyFile(src, dst string) error {
handle err Error {
r, err := os.Open(src)
defer r.Close()
w, err := os.Create(dst)
defer w.Close()
_, err = io.Copy(w, r)
err = w.Close()
case err != nil:
return err
}
}
Example: Error handling without return
In Golang:
func main() {
hex, err := ioutil.ReadAll(os.Stdin)
if err != nil {
log.Fatal(err)
}
data, err := parseHexdump(string(hex))
if err != nil {
log.Fatal(err)
}
os.Stdout.Write(data)
}
In this proposal:
func main() {
handle err Error {
hex, err := ioutil.ReadAll(os.Stdin)
data, err := parseHexdump(string(hex))
os.Stdout.Write(data)
case err != nil:
log.Fatal(err)
}
}
Handling non-error variable
It is possible to handle non-error variables, like:
func doSomething() error {
handle i int {
i = func1()
i = func2()
case i == -1:
return fmt.Errorf("Error: out-of-bound")
case i == -2:
return fmt.Errorf("Error: not a number")
}
}
Nested handle scopes
There are some hard decisions to make the nested handle scopes works properly. So, this proposal suggests to forbid nested handle scopes.
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 118
- Comments: 34 (5 by maintainers)
there’s nothing wrong with go error handling, don’t ruin things for the rest of us
As much as this simplifies the error handling flow, it also hinders readability. One of the biggest advantages of handling errors after each function is that you can scan a file and understand exactly which functions might return errors and how those errors are handled.
The syntax is really odd, with statements before the case. Is not really comparable to the switch/case statements. It looks more like try/catch where you didn’t want to use the try/catch keywords 😃
I feel like people really don’t like (or grasp) the concept that errors are just values. Would you do this pattern for handling bools, ints, strings, etc? I certainly wouldn’t create some block within a function that handled those values. Why are we trying to treat errors as something special?
There being “nothing wrong with go error handling” is completely subjective. Myself and some other gophers believe Go’s current error handling it is too verbose and distracting and that Go would benefit from a change here.
This proposal appears to add an additional way to handle errors but doesn’t appear to force you to change the way you currently code. It might not be an ideal solution but it’s interesting enough to read the proposal and see the discussion. It’s definitely not rising to the level of “ruining” the language even for those who oppose changing Go.
How does this handle the “locality” of the error ?
How do I know if I got the error in the first statement or the second ?
I guess in most cases one would be calling different methods that return different errors (that may or may not be easy to reconnect to the statement that generated the error) but at least in this particular example that wouldn’t work.
This proposal is horrible. It doesn’t look or feel like Go and it hides the origin of the error. Errors are just values, treat them as such.
Is it really worth it to include as a language feature then, just for that use case? Even then, it makes a program using this proposal harder to reason about in error states.
I do not agree with your statement. Errors are something special. Errors are, in a sense, a representation of a non-complete computation.
The proposed
handlestatement looks vaguely like aswitchstatement, with a similar syntax and use of the keywordcase. But the execution is nothing like aswitchstatement. That seems potentially confusing.FWIW, quite some time ago I came up with an error handling syntax that (1) keeps all existing code working and (2) handles about 40-50% of the boilerplate error code in my code. Two (or three) language changes.
It would reduce this
to this
Here’s the gist:
With one more change you could allow passing a string into the error handler at the call site:
x, errHandler("I'm a unique error point: %v", err) := erroringCall()If you think that Go’s design philosophy was correct and works well for your uses then I agree. If you believe, like I do, that Go’s design philosophy was good but not perfect and could now use a bit of correction then adding or changing features seems appropriate.
Go’s philosophy for errors was that they’re values to be be coded against like any other conditional return value. This turns out to be correct some of the time but not all the time. Or maybe all of the time if you’re really wedded to wanting them to be. I think Go would benefit from considering them from additional perspectives too.
It forces the same wordy construct over and over. It’s my opinion, of course, but dropping if err != nil { return err } and its variations all through the happy path harms readability and understanding.
If I’m not going to be able to do anything about an error or exception but essentially pass it up the call hierarchy then I’d rather have something that’s a bit off happy path and would leave the non-exceptional logic intact. If you believe demarking each place where a potential error can occur is important to reading the code then you’re probably more inline with the Go’s original intent. I think the original intent was an interesting idea (even ideology) but hasn’t turned out as well in practice. I’m not opposed to changing the language at this level to add more practical options.
Note: there are already a couple patterns that can help with this but they’re certainly not as elegant as what could be achieved by integrating a solution within Go.
that sounds good!
I think try/catch is excellent for certain types of errors. Database errors in particular but errors where you need to ensure resources are cleaned up work well with try/catch.
Try/catch is nice because it sits a bit above the happy path. One try can hold a half dozen error points so you often don’t need to think about the error conditions at all in that section of your code. If you hit the catch you get the error so you can inspect if if that’s of value. You also can pick up the stack trace too and use it or pass it along as appropriate. There are a couple ways to simulate try/catch in Go already, but they involve defer and defer can suffer from scoping and timing edge cases. I don’t think they’re as clear.
That said, try/catch is horrible for so many classes of errors I can understand why Go’s designers wanted to try another way. For me, I’m not opposed to having multiple ways of handling errors in Go. “x, err :=” is nice for many (most) situations. But I would like it if a less verbose version were available in Go. Errors/exceptions are diverse enough I don’t think a one size fits all solution is going to be found in the Algol language tree.
In my opinion the original Go2 proposal for error handling didn’t go through because, like this, only solved one of the use cases that people wanted (reduce the boiler plate).
Turns out developers wanted more: reduce the boilerplate, wrap errors, etc.
Before coming up with another proposal for error handling we should really first decide what is the minimal set of expectations that a proposal for new error handling should satisfy.