Description
updated based on the feedback from @DisposaBoy
This proposal is related to #32437. I opened a new issue for it, to not side-track the discussion that is ongoing on the main thread.
TL;DR: keep the current proposal largely intact, but use explicit error handling with a Context.
Background
In the design iterations discussion, @griesemer commented on the concept of user-defined error handlers. That concept was dismissed largely due to these concerns: What should happen if the handler is provided but is nil? Should try panic or treat it as an absent error handler? What if the handler is invoked with a non-nil error and then returns a nil result?
Suggestion
I'd like to suggest to revisit this concept with a few adjustments.
Rather than accepting a handler that takes and returns an error (as suggested initially), consider to use a void function that takes a Context. The Context would be an interface like this:
type Context interface {
Err() error
Error(err error)
Abort()
Dismiss()
}
And used like this:
handler := func(c Context) {
c.Error(fmt.Errorf("foo failed: %v", c.Err())) // wrap error
}
f := try(os.Open(filename), handler)
When the handler is defined the c.err is set by try
and the handler is called. This internal value is available through c.Err() and can be replaced with c.Error(error).
When no handler is defined, or the handler is nil, try
will directly return the error. So these are equivalents:
f := try(os.Open(filename))
var handler func(c Context)
f := try(os.Open(filename), handler)
The use of a Context would also allow developers to chain together handlers, similar to the way Gin Gonic handles routes:
func foo() error {
logger := func(c Context) {
log.Println(c.Err())
}
handler := func(c Context) {
c.Error(fmt.Errorf("foo failed: %v", c.Err())) // wrap error
}
f := try(os.Open(filename), handler, logger)
}
The handler would implicitly fall through without the need for c.Next(). If try
hits a nil handler it will return with the value of c.err.
Explicitly aborting the Context would stop the handler chain and have try
set the values to whatever the original function returned:
func bar() (string, string, error) {
return "first", "second", fmt.Errorf("warning")
}
func foo() error {
handler := func(c Context) {
log.Println(c.Err()) // log the non-critical error (warning?)
c.Abort() // abort the error handler
}
a, b := try(bar(), handler)
// a == "first" and b == "second"
}
This allows for the backward compatibility where a function would return a non-critical error and valid values.
Calling c.Abort() is the only way to break the handler chain and have try
not return with an error.
If developers explicitly set c.err to nil
using the Dimiss() function, try
would handle this as any other situation and pass nil
down the chain. At the end of the chain try
returns with the exact value of c.err.
func foo() error {
handler := func(c Context) {
log.Println(c.Err()) // log the non-critical error (warning?)
c.Dismiss(l) // dismiss the error
}
a, b := try(bar(), handler) // returns "nil"
}
If this behaviour is undesired, c.Error() could reject a nil
value and c.Dismiss() could be removed from the Context interface altogether.
With this proposal this short-hand code would still be possible:
func printSum(a, b string) error {
fmt.Println(
"result:",
try(strconv.Atoi(a)) + try(strconv.Atoi(b)),
)
return nil
}
I hope this concept addresses some of the original concerns of the explicit error handling with try()
, while retaining the benefits of the current design, and allowing for alternative scenarios that don't obfuscate the code clarity.
Activity
DisposaBoy commentedon Jun 29, 2019
What's a
Context
?mibes commentedon Jun 29, 2019
There are various implementations, but this gives a good sense of the concept: https://godoc.org/context
You can also check out https://godoc.org/github.com/gin-gonic/gin#Context for an alternative implementation.
DisposaBoy commentedon Jun 29, 2019
@mibes Sorry, I wasn't clear (I know what a
context
is).In your proposal you use
func(c Context)
but there's no discussion about where the nameContext
comes from, its type definition or semantics. And if all it has in it is.Error
then why is it better thanfunc(*error)
?mibes commentedon Jun 29, 2019
Fair enough. The key difference with the original proposal is that although the Error in Context can be nil, the Context itself can not; it is always set and can be passed down to the handlers in the chain.
I'll update the proposal based on your feedback. Let me know if it is still unclear.
Thanks!
urandom commentedon Jun 30, 2019
Not sure why this should be an interface, the proposal does not state if and whether it would have different implementations, and what roles they would play.
It seems much easier to just have a handler like this:
in order to support your abort/dismiss ideas. The equivalent of your dismiss would just be returning nil as the error. The second, boolean return argument, would represent an abort, if that is really needed.
mibes commentedon Jul 1, 2019
The concept of wrapping the error in a Context is that it does not need an explicit action from the developer to return the error. If the error is not explicitly updated, the function just "falls through" with the original error still in place.
As mentioned, the framework could implement the interface in such a way that the error can never be
nil
. c.Error() could reject a nil value and c.Dismiss() could be removed from the Context interface altogether.ianlancetaylor commentedon Jul 9, 2019
This seems like a great deal of special purpose machinery to add just for error handling. While doing something for error handling may well be worthwhile, we still don't want to add too much additional complexity. If this could be in a separate package it would be more acceptable, but as far as I can see it must be built into the language itself.
lunny commentedon Jul 11, 2019
I think it's useful to give the function name. That will make it's easy to be used by all try.
or just remove Context
mvndaai commentedon Jul 19, 2019
This wouldn't remove boilerplate for me because I like wrapping errors. I would need a new handler function for each error. Each line would become something similar to:
I like that the error is contained to the scope of the handler function, but since it is a function the ability to handle in other ways like
break
no longer works.donaldnevermore commentedon Jul 20, 2019
I want to read the code fluently. The
try()
wrapped it and I have to unwrap it in my mind. That would be an overhead.mibes commentedon Jul 29, 2019
Thanks for all your comments. I've closed this issue, because the referred proposal #32437 has been closed.