-
Notifications
You must be signed in to change notification settings - Fork 17.9k
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: the #id/catch error model, a rethink of check/handle #27519
Comments
Go error handling works fine today, no new Some new general-purpose features (macros?) would ease error handling. |
Mix of Go1 & Go2 idioms is not a problem. |
I don't like some kind of "Error-Handle", especially:
It is hard to find which handle will catch the error unless count the code above. And it is not possible to specify the error handle. |
I am surprised to see this posted here as a github issue. Can we have more transparency and guidance on the go 2 draft proposal feedback process? As it stands, we have only been directed to leave feedback on a wiki with no indication of future steps. |
Yes, we prefer feedback on the wiki. Thanks. That said, this seem less like feedback on the design draft, and more like a completely different proposal. |
A major issue with this is the same as with func errorProne() {
x, #err := oops() // goes straight to `catch err error`
i := 10 // define variable i
catch err error {
fmt.Println(i) // can't print i, as it was never defined, even though it is in-scope
}
} Currently, Also, what about this? func errorProne() {
x, #err := oops() // err not nil
y, #err2 := oops2() // err2 not nil
fmt.Println(0)
catch err error {
fmt.Println(1)
}
fmt.Println(2)
catch err2 error {
fmt.Println(3)
return
}
fmt.Println(4)
} What order do these print in? Because it looks like the program runs top-down, does it print |
What if it gets treated like a variable? |
Re Re N.B. in #id/catch draft 1, you need @DeedleFake, yes the handler parameter is only accessible in that handler. |
@xiaoxubeii, lots of people agree, see Named Handlers Are Popular above. #id/catch draft 1 offers implicit (i.e. automatic) handler chains as in check/handle, and explicit ones where one handler forwards the error to another by name. However I believe implicit chaining should be dropped, because it is not obvious when chained handlers are far apart in a function. |
It's not "fixed", it's just expanded upon. You can now declare variables under that construct, but you can't use them outside of the range between the
Assuming you mean "flagged" as in give a compiler error (as there are no warnings in Go), would the following be illegal? func errorProne() error {
x, #err := oops() // err not nil
y, #err := oops2() // err not nil
catch err error {
return wrapErr("error in errorProne", err)
}
} Since If so, then all you've done is replaced
Okay, so I understand the flow now, but I'm not sure I like it. In order to understand it, my eyes need to dart all around the function in order to follow it, when it should be going a single direction. The nice thing about Going back to the original draft -
I don't like this - I want my returns to be within the function... a function's flow should not be determined by something outside of the function (ever). Also, how would this work with multi-value returns? Again, my main gripe with the |
@ianlancetaylor the majority of the feedback on the wiki would probably qualify as a new design. All of it solves the same problem overview scenario, and most contains a new control-flow operation and a technique for re-usable handlers. |
Re Re Re package-level handlers, check/handle has a default handler, which magically returns your function; #pkg is rather similar, just customized. Sure #id/catch can skip over stmts after #id assignment and in catch blocks. If/else/break/continue also skip stmts. Not seeing where your eyes are darting :-) |
@ianlancetaylor, I suggest that the check/handle proposal needs its own issue. Maybe require participants to have posted on the wiki? And designate @gregwebs as a moderator there? |
We don't want a single issue discussing the check/handle proposal. It might be appropriate to have issues discussing specific aspects of it, with absolutely no suggestion of new proposals on those issues. If we permit any issue discussion to introduce arbitrary new proposals for error handling, we will soon have a repeat of #21161, which simply isn't helpful. If we want to discuss new proposals in this area, each new proposal needs a separate issue. |
Re The trace diagram is delightful but inaccurate. "Resume" means continue execution after the catch block. Which is how catch works in other languages. I'll clarify the Feature Summary on that point. (But note that the behavior you imagined is similar to using a locally-defined function :-) |
func errorProne() {
x, #err := oops() // err not nil
y, #err2 := oops2() // err2 not nil
fmt.Println(0)
catch err error {
fmt.Println(1)
}
fmt.Println(2)
catch err2 error {
fmt.Println(3)
return
}
fmt.Println(4)
} So this works like a locally defined function (minus having a func (db *Db) GetX(data []byte) (int, error) {
n, #_ := db.name() // return our own errors via default handler
f, #err := os.Open(n) // this file's presence is optional
defer f.Close()
_, #err = f.Seek(42, io.SeekStart)
l, #err := f.Read(data)
#_ = db.process(data)
catch err error { // handle OS errors here
if !os.IsNotExist(err) { log.Fatal(err) }
log.Println(n, "not found; proceding")
#err = nil // resume (should be the default?)
}
return l, nil
} But this doesn't because of the Do you see my confusion here? Do you see why I'm a being a bit critical? I've discussed most of the points about this proposal already, so I think I'm going to stop here. I just think it's hard to read and hard to follow, and it just doesn't feel like Go to me. I do like the idea of having named error handlers, though! |
The two cases do not work differently. See my N.B. (Latin for "note well, dear reader") above. Agreed that |
I prefer a simple and unambiguous solution. |
Draft-2 complete, as of 2018-09-19. Discussion above pertains to draft-1. |
This introduces a lot of complexity unnecessarily. It is definitely not Go's way. This is not to say that I agree with the var err error
err = foo()
if err != nil {
f()
}
err = bar()
if err != nil {
g()
} which would become: handle err {
f()
}
catch foo()
handle err {
g()
return
}
catch bar() How the latter is any better, I don't know. And the |
I would not introduce special symbols in the language. Go marks exported fields and functions with capital letters (log.Fatal is exported, log.innerFuction is not exported). This would be a nicer way to go:
In other words, if the variable is "fatal", "ret", "stderr", ... and starts with an upper case letter, then it is an #err id. Surely this idea needs more work, but I believe the point of not introducing symbols (non word characters) in the language is important. |
personnaly, I prefer having a clear keyword instead of increasing the semantic load on something else. The check keyword appeared many times, inline sgtm too. My preferred way is
|
A feeling keeps coming back that many proposals, this included, are not abstract enough. There are attempts to address either specific value types, or a limited number of return values (commonly 1), or reuse/fit concepts from other ecosystems. But errors and returned errors among multiple values are just values and collections of values. I am contending that we are trying hard to address a narrow case of a generic problem and are failing to cover all nuances which may otherwise derive from broader principles. So let's have a proposal which does not even mention errors - but addresses them as just one application of general improvement. To abstract it out (slide 7 in the deck you linked), we are looking to have:
Any modification of the language will bring a pile of corner cases to consider. Any modification will bear a whole new pattern of the language use, that is the purpose. And, yes, a more generic line of thinking may bear "more different" use patterns, and it may be scary, but it may also allow for fewer corner cases. I also encourage to consider speaking in more abstract terms than ideas of throwing and catching, which carry a cognitive load from other languages. |
Hi Vlad, thanks for the feedback. Note that this comment appears in the first Feature Summary example: // a non-zero value for #id triggers corresponding catch So yes, a single
@Alpt & @mcluseau, I'd be glad to see any variation of this scheme adopted, regardless of the assignment syntax. So yes, your variations are worth considering! |
Hi Liam, yes, I understand that this proposal is for "handlers" (for a lack of an agreed term) of a single value of any type. It is for a single value though. As I mentioned, that is a strange (to me) limitation. That does not allow for a parametrized "handler" so that it can get more information. It does not allow for a single "handler" to process multiple values if returned by Philosophically I would like any number of "bindable" places on an assignment's left to belong to "handler" calls - and maybe the rest assigned to a variadic thingy. So in a synthetic case of v11, v12, a1(a11,a12, a13), b1(b11) := f() // a1 is ternary and b1 is unary, the order of call is from right to left
a2(a21), b2(b21, b22), v21... := f() // a2 unary, b2 binary, v21 gets a 3-long slice
// etc Essentially you can think of what is on the left of the assignment in a similar way to what is inside the parenthesis of a function call. It is a very familiar concept and neither people do nor tooling has issues with it. It is strange for me to have a special shortcut definition syntax for such "handlers". Mimicking function definitions with another keyword instead of I am not saying that this is THE solution. I am saying that we are thinking at a wrong abstraction level. And even if single-value It also signals back to languages where there are |
Any other variables set by the assignment that invokes the handler would be visible in the catch block. If you don't like catch blocks, well, I dunno what else to say :-) |
@networkimprov I think we're pretty close indeed in our intentions: it's about going through inlined code common to a block or func. I initialy used @didenko the problem with a more abstract concept is that the number potential of miss-use cases increases exponentially. |
Mikaël, my concept is a feature that anyone who's written The variation |
@networkimprov I agree on the keywords in assignment, that was a suggestion I kept by default but it's totally not mandatory to have it, as I explained in my edit (if you only read e-mails you may have missed that ^^). We could also use a |
Of course, it does - especially if "misuse" is defined in the boundaries of narrow error handling patterns. My whole point is that error handling is pointing to a need for a significantly different flow mechanism conceptually than what is available in mainstream languages. You catch all the "catch" hate when you copy "catch" from other languages, so to speak. But if you define a new legitimate way of control flow, which also allows you to handle errors as one of its applications, then other uses are no longer "misuse" necessarily. They may be a good or a bad practice as code design is concerned, but not a misuse as "used not like it was intended". Because the intentions are not narrowly bound. Anyhow, I shall not post here more. |
@didenko I understand your point, I personnally am in the "common code triggered by assignment in the function scope" line, were error handling is a particular case too, at seems not so far from yours. |
@ianlancetaylor would you be willing to include this (and perhaps others offering local error handlers) in #40432? Also the issue could note that a handful of proposals remain open; i.e. not declined. |
@networkimprov I didn't notice a general theme of proposals with local error handlers, but I could easily have missed it. Can you point me at a few others? I don't think the meta issue needs to point out which proposals are open or closed, that will just require more updating. The information is inherently available in the issues themselves. |
For proposals with local error handlers, see the item on this wiki page labeled "Invoke one of several handlers by name". They're mostly not on Github, because the check/handle document specifically requested feedback via the wiki. https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes In any case, could you add this proposal (#27519) to #40432? |
OK, done. |
I just noticed this proposal. It's similar to the one I just posted, #48896, but mine may be more intuitive. |
I would like you to consider this suggestion, it is similar to this one. It basically adds a catch syntax in which the first func CopyFile(src, dst string) error {
r := os.Open(src) catch
defer r.Close()
w := os.Create(dst) catch
io.Copy(w, r) catch { w.Close(); os.Remove(dst) }
w.Close() catch { os.Remove(dst) }
return nil
catch (err error):
return fmt.Errorf("copy %s %s: %v", src, dst, err)
} |
Based on historical decisions with regards to error-handling, this proposal is bound to fail.
All these concepts are introduced with a new syntax leading it to be classified as cryptic, but also complex. A new developer requires explanation to understand it, and readability suffers as indicated by #27519 (comment) and #27519 (comment). It doesn't simplify the verbosity of error handling, as per the original error-handling draft design. It treats errors as special values? However, it's backwards compatible and allows wrapping. |
Hi, the comment you reference was based on a misunderstanding, which I clarified in comments following it. |
This comment was marked as spam.
This comment was marked as spam.
I think this is the most viable error handling proposal open today |
Please do not down-vote this post if you are against any new syntax for error handling.
Instead, vote in the first comment below. Thanks!
Having heard users' frustrations with Go1 error handling, the Go team has committed to delivering a new method. Ideally, a solution would stem from a familiar language. The Go2 Draft Design is fine for wrapping errors with context, and returning them succinctly. But its feel is novel, and it has significant drawbacks, discussed in Golang, how dare you handle my checks!
Besides returning errors, Go programs commonly:
a) handle an error and continue the function that received it, and
b) have two or more kinds of recurring error handling in a single function, such as:
There is indeed a long list of Requirements to Consider for Go2 Error Handling. The
check/handle
scheme accommodates a tiny subset of these, necessitating an awkward mix of Go1 & Go2 idioms, e.g.Herein is a widely applicable approach to error handling, leveraging the C-family catch block. For the record, the author is grateful that Go does not provide C++ style exceptions, and this is not a plot to sneak them into the language through a side door :-)
The
#id/catch
Error ModelLet a catch identifier (catch-id) e.g.
#err
select a named handler. A single catch-id may appear in any assignment. A handler is known by its parameter name; the parameter can be of any type. A handler follows the catch-id(s) that trigger it and starts withcatch <parameter>
. Catch-ids are not variables and handler parameters are only visible within handlers, so there's no re-declaration of error variables.These are not unique ideas. At last count, 17 posts on the feedback wiki suggest various ways to define and invoke named handlers, and 13 posts suggest invocation of handlers using assignment syntax.
Several points are unresolved, see Open Questions below. Catch-id syntax is among them;
#id
is reminiscent of the URL form for goto id, but?id
,@id
, and others are viable.Advantages: similarity to established try/catch method (but without a try-block); clarity as to which handler is invoked for a given statement; certain statements may be skipped after an error occurs; handlers can return or continue the function.
Please help clarify (or fix) this proposal sketch, and describe your use cases for its features.
Feature Summary
Draft-2, 2018-09-19. Discussion following this comment below pertains to this draft.
These features meet a large subset of the Requirements to Consider for Go 2 Error Handling.
We can select one of several distinct handlers:
We can invoke a handler defined at package level (thanks @8lall0):
We can specify a type for implicit type assertion:
We can skip statements on error and continue after the handler:
We can forward to a different handler (creates an explicit handler chain):
We can reuse catch-ids:
We can nest catch blocks:
We can see everything from the scope where a handler is defined, like closure functions:
We can still use Go1 error handling:
Open Questions
What catch-id syntax?
#id
,?id
,@id
,id!
,$id
, ...What style for predefined handlers?
#r
,#p
,#_
,#return
,#panic
,#nil
, ...What handler definition syntax?
catch id [type]
,catch id(v type)
,id: catch v [type]
, ...Infer parameter from previous stmt?
#err = f(); catch { log.Println(err) }
Invoke handler when ok=false for
v, #ok := m[k]
|x.(T)
|<-c
, etc?Pass a type
error
with context?v, #err := m[k]; catch { log.Println(err) }
Treat parameter as const?
catch err { err = nil } // compiler complains
Lets forwarding skip test for nil:
catch err { #ret = err }
Require
#id
for return values of typeerror
? proposal: spec: require return values to be explicitly used or ignored (Go 2) #20803Provide
check
functionality withf#id()
? e.g.x(f1#_(), f2#err())
If so, disallow nesting?
x(f1#err(f2#err()))
Allow position selector?
f#id.0()
tests first return valueProvide more context to package-level handlers, e.g. caller name, arguments?
catch (pkg error, caller string) { ... }
Allow handlers in defer stack?
Allow multiple handler arguments?
Disallowed Constructs
Declaring or reading a catch-id:
Multiple catch-ids per statement:
Shadowing of local variables in handlers:
Self-invocation:
Unused handlers:
Discarded Ideas
Chain handlers with same catch-id in related scopes implicitly, as in the Draft Design:
Changelog
2018-09-19 draft-2 (discussion below)
a) Move implicit handler chain to new section "Discarded Ideas".
b) Make continuing after catch the default behavior.
c) Document catch-id reuse and nested catch block.
d) Disallow unused handlers (was "contiguous handlers with same catch-id") and self-invocation.
e) Add
#_
predefined handler to ignore or log input.f) Add implicit type assertion.
Why Not
check/handle
?Please read Golang, how dare you handle my checks! for a discussion of each of the following points.
handle
chain cannot continue a function.check
is specific to typeerror
and the last return value.check
operator can foster unreadable constructions.handle
chain is inapparent; one must parse a function by eye to discover it.Also, there is relatively little support for the draft design on the feedback wiki.
Named Handlers Are Popular!
At last count, roughly 1/3rd of posts on the feedback wiki suggest ways to select one of several handlers:
And the following posts suggest ways to invoke a handler with assignment syntax:
/cc @rsc @mpvl @griesemer @ianlancetaylor @8lall0 @sdwarwick @kalexmills
Thanks for your consideration,
Liam Breck
Menlo Park, CA, USA
The text was updated successfully, but these errors were encountered: