-
Notifications
You must be signed in to change notification settings - Fork 18k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: spec: error handler operator ?() #72149
Comments
Edit: copied in cases from pre-work. |
Edit2: update description according to this diff.
|
Thanks for the extensive list @apparentlymart . I will need to go through those systematically. This proposal is really written to handle the most important use cases for #71203 in an expression based solution. The importance of making it expression based, being to allow usage within chained API calls and inline struct assignments. I haven't done much work yet comparing it to the |
PS! Other people than me have come up with more or less the same idea in previous comments either on #71203 or on the associated discussion:
As far as I can tell, they came up with it independently. Both links include discussions. |
Looking at #32437 (comment) first:
I agree to this comment in the case of the Current codefunc CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if _, err := io.Copy(w, r); err != nil {
w.Close()
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if err := w.Close(); err != nil {
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
} Code with ?()func CopyFile(src, dst string) error {
r := os.Open(src) ?(errors.Wrap("copy %s %s: %v", src, dst))
defer r.Close()
w := os.Create(dst) ?(errors.Wrap("copy %s %s: %v", src, dst))
io.Copy(w, r) ?(func(err error) error {
w.Close()
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
})
w.Close() ? (func(err error) error {
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
})
} AssessmentObviously the returns of the error becomes less visible with the new syntax; which I suppose is part of the point. I don't necessarily find the flow less clear. However, both versions here actually leaves a score of errors unchecked. We could try to deal with this in both variants by joining errors. Neither case is currently set-up to handle errors from deferred calls. I think that would be interesting to explore Will spend some time investigating the case in the linked repo once I got time. |
i think we should still having to declare the full return of the function, and do the callback explicitly. func wrapWithFnameError(fname string, err error) error {
err = fmt.Errorf("%s: something went wrong: %w", fname, err)
return err
}
type SomeStruct struct {
Param1 int
Param2 int
}
func MyFunction() (SomeStruct, error) {
param1, param2, err := createParams()?(wrapWithFnameError("MyFunction", err))
myStruct := SomeStruct{param1, param2}
return myStruct, nil
} also: what happens if you have a function that can return multiple errors? just chain the ? operator.
this is how the situation will looks. func funcWithMultipleErrors() (int, error, error, error) {
// ...
}
func caller() error {
value, err3, err2, err1 := funcWithMultipleErrors()???
fmt.Println(value)
return nil
} each "?" really gives to the next one all params except the last one if is of type error. so, the first ? will return if err1 is not nil, the second ? will return if err2 is not nil, and so on. now, doing the previous code i had an idea: what if ? not always returns early? let me explain. func validateConfig(config AppConfig) error {
var configErrors error
dbErrors := config.DB.Validate()
if dbErrors != nil {
configErrors = errors.Join(configErrors, dbErrors)
}
cronErrors := config.Crons.Validate()
if cronErrors != nil {
configErrors = errors.Join(configErrors, cronErrors)
}
someOtherErrors := config.Other.Validate()
if someOtherErrors != nil {
configErrors = errors.Join(configErrors, cronErrors)
}
// now, if no errors were found, configErrors will be nil.
return configErrors
} you don't want to return the first error you encounter, you want to check for all errors before return them as a whole, so, this is what i thought: func newValidateConfig(config AppConfig) error {
dbErrors := config.DB.Validate() ? () // this will assign dbErrors = dbErrors
cronErrors := config.Crons.Validate() ? errros.Join(dbErrors, cronErrors) // this will assign cronErrors = errros.Join(dbErrors, cronErrors)
someOtherErrors := config.Other.Validate() ? return errors.Join(cronErrors, someOtherErrors) // now, this one with the return will effectively return early
return nil
}
if you are doing just one handler, the parenthesis may be omitted (like in function return types). oh, and if you have multiple handler functions, the return goes outside the parenthesis: err := myFunction() ? return (handler1, handler2, handler3) about defers...
so... the ? can't either assign nor early return, since you are already returning. the only use i think for using ? with a defer call is for doing something like log or panic if it had an error, but that may need an implicit variable for the error param, etc. |
@nullyfae , If I understand you correctly, you are suggesting a I will. try to answer the other questions.
This isn't really a case that's handled. The proposal is to only allow cases where there is either one or two return paraemeters, including the error:
If there is a function with two error returns, that's technically allowed, but only the last one will be handled by the Other cases, including the one you mention with
It doesn't. The There is a case of collecting errors in the "Cases" section of the description; please have a closer look at that.
There is actually three things we can do with
If we go for 1 or 2, the application writers can choose to write |
This should not be considered a formal Go proposal yet, but the pre-work needed in order to create one. When all requirements are in place, this may transition into a proposal.
Contribution guide:
Contributions that may be of particular interest for now:
Links:
Remaining content is aligned with the issue text.
Go Programming Experience
(Contributors may add their own replies)
@smyrman: Experienced
Other Languages Experience
(Contributors may add their own replies)
@smyrman: C++, C, Python
Related Idea
Has this idea, or one like it, been proposed before?
The proposal is inspired by Go discussion #71460. Compared to the discussed proposal, this is similar in syntax, but different in semantics.
Key differences to #71460:
?{...}
, we use syntax?(...)
.?
in the proposal acts more like a normal function call.Key similarities to #71460:
?
character.Semantically, this proposal is somewhat similar to try-catch proposal, but simpler. The syntax and ergonomics are different.
Does this affect error handling?
Yes
This proposal includes:
errors
standard library package.Is this about generics?
It's not about generics, but the proto-type is using generics for it's implementation.
Cases
Before discussing the proposal, we will demonstrate a few use-cases that could benefit from it. The cases will be relatively simple. Real use-cases may be more complex, and could therefore expect to result in saving more lines.
Return directly
The direct return of an error is a commonly used case for error handling when adding additional context is not necessary.
Old syntax:
New syntax:
Return wrapped error
To wrap an error before return is a commonly used case for error handling when adding additional context is useful.
Old syntax:
New Syntax:
Collect errors
The case for collecting errors is likely not common in library code. However, it is likely useful for application code. Possible use-cases include form validation or JSON APIs.
Old syntax:
New Syntax:
Custom error wrapping
Custom error type:
Old syntax:
New Syntax (inline handler):
Proposal
The proposal has two parts:
?()
/?
to catch errors.errors
package.The proposal follows the principal of the now implemented range-over-func proposal in making sure that the solution can be described as valid Go code using the current language syntax. As of the time of writing, this is the syntax of Go 1.24.
The
?
/?()
syntax can be used to move handling of errors from the left of the expression to the right. The default handler (no parenthesis), is to return on error. When parenthesis are provided, errors pass though handlers of formatfunc(error) error
. If any handler return nill, the code continuous along the happy path. If the final handler returns an error, the function with the?
syntax returns.It's not yet clear if the
?
syntax should be allowed inside functions that does not return an error. If it's allowed, the suggestion is that the?
syntax would result in a panic. See options for more details.The standard library changes involve adding handlers for the most common cases for error handling.
Standard library changes
The following exposed additions to the standard library
errors
package is suggested:Language Spec Changes
No response
Informal Change
The proposal introduce a new
?
operator, which can be used after calls to functions that has any of the following signatures:The syntax of
?
is similar to that of a function call, except the parenthesis()
are optional. That is?
and?()
are equivalent. The signature of the operator can be described as:When using the
?
syntax, the last return parameter of the function is passed to the?
operator to the right, instead of to the left as normal.The processing rules for error handlers is as follows:
If the
?
operator receives anil
error value, execution continues along the "happy path."If the
?
operator receives an error, the error is passed to each handler in order. The output from each handler becomes the input to the next, as long as the output is notnil
. If any handler returnnil
, the handler chain is aborted, and execution continues along the "happy path."If after all handlers are called, the final return value is an error, then the flow of the current statement is aborted similar to how a panic works. If
?
is used within a function where the final return statement is an error, then this panic is recovered and the error value is populated with that error value and the function returns at once.Is this change backward compatible?
Yes
This work leans on the work done for %71460, that highlights that the
?
operator is invalid to use in any existing code. Thus it's expected that no existing code will be able to break due to the introduction of the new syntax.Orthogonality: How does this change interact or overlap with existing features?
No response
Would this change make Go easier or harder to learn, and why?
Any addition to the Go syntax, including this one, will make it harder to learn Go. However, people coming from an exception handling paradigm may find the new syntax less intrusive then the explicit return.
Cost Description
The highest cost of this proposal is likely that there will now be multiple patterns for handling errors. There could be discrepancies and disagreement between different projects about which style to use.
Changes to Go ToolChain
vet, gopls, gofmt
Performance Costs
No response
Prototype
The proto-type code is found in the pre-work repo.
Following the example of range-over-func, the implementation of the
?
semantics is not magic. A tool could be written to generate go code that rewrites the?
syntax to valid go 1.24 syntax.With proposed syntax:
Can be written using the proto-type library as:
We defined the following functions in the
xerrors
package for our proto-type. This is proto-type code only. The final implementation will likely be handled by the compiler directly:Options
Options could be applied to change the proposal in various ways.
1: Disallow usage within non-error functions
We could choose to disallow the
?
syntax inside functions that doesn't return errors. This included themain
function.This would ensure that the
?
syntax can not lead to panics.2: Allow explicit catch
An option could be to expose the
Catch
function from the proto-type, and allow supplying a set of error handlers that run on all errors.When an explicit Catch is added, then an implicit Catch is not added.
If the Catch is called with a nil pointer, then any error that isn't fully handled (replaced by
nil
), results in a panic.3: Chain via ? syntax
Use syntax
? handler1 ? handler2
as shown in this comment by @733amir.Why not...
Why not allow more than two return values?
Most functions that return an error, return either a single parameter, or two parameters. So it wouldn't be many cases where it's useful. It's also assumed that error handling syntax is mostly useful if it allows to continue the flow of our programs. That is, we allow are allowed to chain functions
A()?.B()?
, or assign to struct fields from functions that return errors. Cases with two or more return values typically can not be chained.Allowing for more return values risks complicating the implementation, and is likely offer little value in return.
Why require the final return parameter to be an error?
If we allowed for other return values for the naked syntax, it's not clear what the error return value should be.
If we allow for explicit handlers, then we need a conversion from bool to error before we can pass it to the handlers. Thus the argument list changes.
The text was updated successfully, but these errors were encountered: