Closed switchupcb closed 2 years ago
I'm almost afraid to ask, but I assume else
blocks or chained else if
, i.e. return if err != nil { err } else { nil }
, are not allowed?
var ErrBadInput = errors.New("bad input")
func Foo() error {
err := CanReturnMultipleErrorTypes()
return if errors.Is(err, ErrBadInput) { fmt.Errorf("invalid input: %w", err) } else if err != nil { err } else { nil }
}
Pretty nasty looking.
@deltamualpha Correct, else if
or else
chains should NOT be allowed; justified by classifying return if
as a keyword.
Edit: Note that this maintains explicitness.
func Foo() error {
err := CanReturnMultipleErrorTypes()
return if errors.Is(err, ErrBadInput) { fmt.Errorf("invalid input: %w", err) }
return if err != nil { err }
return nil
}
Originally
var ErrBadInput = errors.New("bad input")
func Foo() error {
err := CanReturnMultipleErrorTypes()
if errors.Is(err, ErrBadInput) {
return fmt.Errorf("invalid input: %w", err)
} else if err != nil {
return err
}
return nil
}
Originally Alternatively
if err := CanReturnMultipleErrorTypes(); errors.Is(err, ErrBadInput) {
return fmt.Errorf("invalid input: %w", err)
} else if
...
The following people have downvoted with no further explanation: carlmjohnson (from #21498 conversation), chrispickard, florianl, BroccoliHijinx, and gmeligio, MojixCoder, seankhliao, ericlagergren, krhubert, and jimen0.
Included to address "community voting".
(moved to proposal)
Potential Supporters (from similar proposals): @schwrzstrbn @urban-wombat @eldandev @FlorianUekermann @l0k18 @perholmes @andreynering @alanfo @mccolljr
I also like this idea. It is similar to Ruby's end of expression if but by placing return if at the beginning of the statement, it becomes much more readable.
Here's my opinion:
Do I find the current state of error handling somewhat unwieldly? Yes.
Does this proposal improve upon the things I find unwieldly? No. I think that a proposal like this is too singularly focused. This simplifies single error returns, but I'm not sure it successfully addresses things like error aggregation or propagation.
I'm glad you're thinking about alternative solutions for error handling. Please continue to do so. I just don't personally feel like conditional return is the solution.
I think that a proposal like this is too singularly focused. This simplifies single error returns, but I'm not sure it successfully addresses things like error aggregation or propagation.
I ask that you correct me where I'm misinformed in the following statement.
I agree that this proposal doesn't consider error propagation or aggregation. However, it's impossible to solve every mechanic with one proposal, without compromising on fundamentals. I also don't believe it's necessary. In the original error-handling draft design, creators of the language state:
These constructs are in the spirit of “errors are values” but aim to reduce the verbosity of handling errors. —(@mpvl @rsc)
Go treats errors as values. The program does not crash when you ignore a string, so you cannot crash when you ignore an error.
Go already has technical stacktraces upon panic
, and business stack traces upon fmt.Errof("%w", err)
. If you want to return a static error, you can use errors.New("")
but these all are ways to HANDLE errors (with respect to the business case).
If error propagation refers to the request to handle errors, that request is wrong. You are NOT required to handle ignored strings, so why would you be required to handle ignored errors? You are NOT. In comment https://github.com/golang/go/issues/52416#issuecomment-1102834515 of https://github.com/golang/go/issues/52416, @sirkon uses a code statement similar to Russ Cox Error Handling Problem Overview to outline the "issue" with errors. What does Russ say about that code?
It is nice, clean, elegant code. It is also invisibly wrong. — @rsc
Go will never support mandatory error handling and thus never support error propagation. You can propagate error values with func
tions similar to passing a string
around a program. You can't say 1 + 1 return 3
and expect Go to fix that for you. Ignoring an error that needs to be handled isn't an issue with Go, but the developer.
The Russ Cox Error Handling Problem Overview states no issue with aggregation.
For Go 2, we would like to make error checks more lightweight, reducing the amount of Go program text dedicated to error checking.
Keep in mind that @rsc starts this same overview with the following statement:
In general Go programs have too much code checking errors and not enough code handling them.
Then proceeds to make a proposal for check
and handle
that reduces the amount of code handling them... To be specific, using a function to handle errors assumes that every error will use the same error message. So he is not handling any aggregation of errors, but rather handling every error with the same message using jumpy syntax. That was 4 years ago.
If aggregation refers to the comparison of errors, we are usually referring to a problem creating by a developer. If I want you to compare 2 strings, I expect you to use the ==
statement. If I want you to compare a custom type (consisting of strings), I expect you to use a provided func
or a pointer
.
An error
is an interface { Error() string }
. When you create an error
variable, you are instantiating a type interface { Error() string }
. Can you compare interfaces using ==
? Yes. Is there a way to compare interfaces? Yes. Is there a way to compare strings? Yes. Is there a way to compare errors? Yes. People are unable to compare errors correctly, when there are already tools in the language to do so.
Why do we need a language feature for this?
I think this proposal is interesting because it would also be useful when returning various values from a function even when not used for error handling.
I used the similar conditional if expression a lot in Ruby before, it is very convenient. Do this is more than just a convenient shorthand for error handling.
it's similar to if-expression in Rust:
if num > 3 {
"Tom"
} else {
"Jerry"
};
Personally, i like this proposal, it's more expressive, but it may also be againsted with reasons like "one way", "keep Go simple", etc..
I think if gofmt was changed to accept one liner if statements none of this would be interesting. I don't like the implicit return syntax of Rust, which I see many clearly referring to, because it's even worse than a naked return on a long function or unnamed multiple return values and 5 different variable declarations scattered over 60+ lines of code. The gofmt solution changes nothing in the compile it just makes a small readability improvement. We can make one liner function declarations, why not one liner for all parenthesised blocks?
I think if gofmt was changed to accept one liner if statements none of this would be interesting. I don't like the implicit return syntax of Rust, which I see many clearly referring to, because it's even worse than a naked return on a long function or unnamed multiple return values and 5 different variable declarations scattered over 60+ lines of code. The gofmt solution changes nothing in the compile it just makes a small readability improvement. We can make one liner function declarations, why not one liner for all parenthesised blocks?
A thousand times THIS! This is purely a formatting problem. In other (more relaxed) languages, you just get all of this on one line, and the problem goes away:
result, err := getFromDatabase(); if (err) return err
Or whatever. It's a hack, but GoFmt could simply make it a rule that if a "return" statement is on a line, it isn't formatted. Instant 70% reduction in code spam caused by error handling.
Yes, but that's what #33113 proposed. And this proposal boils down to adding a new keyword to avoid trying to have that discussion again.
Error propagation
If error propagation refers to the request to handle errors, that request is wrong. [...] Go will never support mandatory error handling and thus never support error propagation.
This isn't really what I was referring to.
A lot of code can generate errors. Syscalls, parsing, validation, network requests, etc - most code we write can in some way produce an error. It is not always clear what that error should be or where our code should handle those errors, especially during initial prototyping.
To take a limited example, think about making an API request - some errors are immediately retryable, some are retryable after some action (i.e. we send a request to refresh an auth token), and some are unrecoverable. If you want to handle those 3 cases differently, you have to pipe around the raw errors and check them. This necessarily makes error
returns a part of your API surface area. You have to add error
returns to every step in the call chain, check those errors at every step in the call chain (if err != nil { return err }
), and then handle them somewhere. Making the wrong choice early on can be an absolute pain to refactor later, as adding an error return to one function that used to enforce an invariant with a panic can be a cascading change that affects all code.
From that perspective, saving a few characters of typing for each intermediate error return doesn't really address my pain point, which is that I have to either: assume I will eventually want to handle an error and add error
returns to my functions even when I'm not currently generating errors, or choose to omit error
returns and potentially have a massive refactor to deal with later when I decide I do need to propagate an error
upwards through a large call chain.
Another common thing that comes up is the need to do a series of N things, where each thing can generate a single value and a single error, and where each thing N depends on the value generated by thing N-1. In this case, I end up writing N lines of function calls and Nx3 lines of error handling. Even if that were simplified to N lines of function calls and N lines of error handling with simplified return statements, the error handling code takes up 50% of the space even though I'm not handling any of the errors locally to that code - I'm just sending them somewhere else to be handled, ignored, reported, or aggregated. If I want to use "default" values at each step, rather than just return the errors? It goes back up to Nx3 lines of code because I can't use the early return anymore. If I need to clean up after the previous steps? I either have to pull this N-step process out into a helper function and use defer
, or I have to manually write that cleanup code at each step. If I have many return values? I have to specify a value for each one, each time I return an error.
I do not want try
/catch
semantics in Go. I'm not sure what form a solution that would address these pain points would take. In any case, these are the things I'm referring to when I say "error propagation".
Error Aggregation
If aggregation refers to the comparison of errors
It does not.
I was referring to these situations:
And I'm not sure that a language feature is needed to address this. There are libraries that help, like multierror
, but these libraries 3rd-party, so there is no standard and some library maintainers roll their own.
In general, I think errors are values
is both a true and a false statement.
It is true, because errors are literally values, in that they can be ignored, collected, moved around, and in general treated as any other value. I think this is a good thing.
It is false, because they are also special in that they have a unique role in a program. They are values, but they are values that are treated specially by most code that uses them. Can an integer be an error? Absolutely. Can a string? Absolutely. Can the empty struct be an error? Again, absolutely. But when that object gets wrapped in error
, it is no longer playing the role of the type it holds - it is playing the role of communicating to the some part of the program that an irregular state has been encountered and something needs to be done, reported, or ignored to continue making progress.
Perhaps a part of this pain point is how general the error
interface is - it is basically the fmt.Stringer
interface but with a fancy method name. 90% of the processing of that value beyond just printing it is left entirely as an exercise to the reader.
it's similar to if-expression in Rust:
if num > 3 { "Tom" } else { "Jerry" };
If expressions in rust can be used anywhere an expression is expected. This includes assignments, in macro and function calls, etc.
This proposal is specific to return statements.
If this were a proposal for some form of inline If expression I would be more supportive - that would be much more general-purpose.
I think such a proposal would be unlikely to pass debate though - the Go team seems like they've firmly made up their minds that such a thing will never exist in the language
why not one liner for all parenthesised blocks? — @l0k18 @perholmes
See Iteration One. One-liners can decrease readability by combining extraneous statements with return statements. In a similar manner to what deltamualpha stated in https://github.com/golang/go/issues/53017#issuecomment-1134595952.
In general, I think errors are values is both a true and a false statement. @mccolljr
The main thing is that in Go, errors are values. It's a fundamental of the language.
A lot of code can generate errors. Syscalls, parsing, validation, network requests, etc - most code we write can in some way produce an error. It is not always clear what that error should be or where our code should handle those errors, especially during initial prototyping.
Again, this code needs to provide a way to compare errors if it is necessary. As an example, network requests usually involve status codes which provide context to an error.
You have to add error returns to every step in the call chain, check those errors at every step in the call chain (if err != nil { return err }), and then handle them somewhere. [...] From that perspective, saving a few characters of typing for each intermediate error return doesn't really address my pain point [...] I end up writing N lines of function calls and Nx3 lines of error handling. Even if that were simplified to N lines of function calls and N lines of error handling with simplified return statements, the error handling code takes up 50% of the space even though I'm not handling any of the errors locally to that code.
The pain point in the other proposals point at the verbosity of error handling being the issue. I understand what you're saying with regards to error handling, but you cannot avoid handling errors because errors are values. I am not sure what you mean by the following statement.
If I want to use "default" values at each step, rather than just return the errors?
You can return the zero value with an error.
The series of N issue — as you have explained it — can only be handled with exceptions (i.e try catch
of a non-zero value). You would have to return the error or treat errors as a special value. In the exception handler, you end up comparing errors since the context is lost.
The following code is from the check
and handle
proposal:
func CopyFile(src, dst string) error {
handle err {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
handle err {
w.Close()
os.Remove(dst) // (only if a check fails)
}
check io.Copy(w, r)
check w.Close()
return nil
}
In any case, handle
ends up being a shorthand for if err != nil
before the statement occurs. If it were implemented as exceptions:
func CopyFile(src, dst string) error {
handle err {
// requires error comparison
if error.Is(ErrOsOpen) {
...
else if error.Is(ErrOsCreate) {
w.Close()
os.Remove(dst) // (only if a check fails)
} else if
...
}
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
io.Copy(w, r)
w.Close()
return nil
}
When errors are values, we are forced to handle (or ignore) the error at the source. As a result, the only way to improve error handling is by making it easier to write (and read).
@switchupcb Indeed, and error handling at the source is good, because the context is kept. With return if
this would become that much easier.
@mccolljr There is also https://pkg.go.dev/errors which is a standard library approach to managing aggregated errors.
@switchupcb sorry for delayed response here.
I'm aware of the errors package, of course, but it isn't actually standard library as far as I am aware, even if it is commonly used.
As for your other points regarding what is/isn't possible with errors in go due to the fact that errors are values:
I'd be likely to support a proposal for novel control flow that could benefit error handling as a side effect. As it stands right now, I see conditional return as a minor space-saving syntax optimization, which I can live without for the time being if it means a more aggressive control flow optimization may be considered in the future.
If this feature were added to the language I might make use of it, but I don't get the sense that it would dramatically improve my experience with error handling as it stands.
The suggestion here is that we replace
if err != nil {
return err
}
with
return if err != nil { err }
The new code has fewer lines but the same number of characters. The syntax, with a value or list of values in curly braces, is unlike any other current Go syntax.
The emoji voting is opposed to this change.
Therefore, this is a likely decline. Leaving open for four weeks for final comments.
why not one liner for all parenthesised blocks? — @l0k18 @perholmes
See Iteration One. One-liners can decrease readability by combining extraneous statements with return statements. In a similar manner to what deltamualpha stated in #53017 (comment).
I know this is over but after reading the replies I'd have revised that to "one liner if without else statements in gofmt" It's the smallest change.
This proposal is not a duplicate, but similar to https://github.com/golang/go/issues/33113, https://github.com/golang/go/issues/27135, https://github.com/golang/go/issues/27794, https://github.com/golang/go/issues/32860, and https://github.com/golang/go/issues/52977. For more information, read Justification.
Code
Iteration 3
Fixes iteration two and scopes the returned variables or functions.
Single
Multi
Iteration 2
Fixes iteration 1 by requiring the user to return a variable. However, this version offsets logic by adding a trailing
if
after the returned variables, in contrast to placing the returned variables AFTER theif
statement (i.eIF err != nil { return ERR }
).Iteration 1
The issue with this version is that it implicitly allows 1-line if statements to be used on a variety of code. The reader will NOT be able to guarantee that the 1-line if statement is returning from the function.
This proposal only supports Iteration 3.
Justification
Is it cryptic? No information is lost while the return statement is made more explicit. Is it complex? It's understandable by new people. Is it worth? error handling + one-line returns codebase %. Is it backwards compatible? Yes. Does it allow wrapping? Yes. Does it pass code coverage tools? if
if
does.https://github.com/golang/go/issues/33113
This statement is made in the context of a now-closed error proposal (
catch
) and implies that the 1-line if statements would apply to all Go code.https://github.com/golang/go/issues/27794#issuecomment-430404518 This statement involves the syntax for the first iteration, which is not what this proposal sets forth. More important is the argument is that there is already one method to conditionally return (preceding
if
), such that a trailingif
is unnecessary; if it only removes a few lines. As a result, this proposal would be further justified if https://github.com/golang/go/issues/21498 passes.Are there any special rules? No special rules are required for error values or named return values (https://github.com/golang/go/issues/37141).
Are errors treated as special values? No, since
return if
can be used on any value (matching fundamentals).Think of the vim users!
return if
requires 9 characters, while a statement such aserr != nil
requires 10, giving Vim users 59 (80 - (19 + 2)) characters of free-width. The cost is 1 vertical line as opposed to 3.Readability
People reading code can distinguish that
if
statements involve further evaluation; as opposed to mixing up error-handle or variable-assignment or any other one-lineif
functionality that is prevalent in Go codebases.Here is an example of error code handling (provided by a comment below) in
go 1.18
:Here is an example with
return if
:PLEASE READ THE FOLLOWING COMMENTS BEFORE COMMENTING https://github.com/golang/go/issues/53017#issuecomment-1133768136
As of 5/24, twenty-one people have downvoted without further explanation. Please upvote the proposal by reacting with a thumbs up. Downvote by stating your objections in a comment or upvoting a similar comment respectively.