The categorization is necessarily imperfect but hopefully still helpful.
Please forgive the occasional Markdown rendering glitches.
It will be updated periodically.
Mail rsc@golang.org with corrections.
I’m in favour of this proposal. It avoids my largest reservation about the previous proposal: the non-orthogonality of handle
with respect to defer
.
I’d like to mention two aspects that I don’t think have been highlighted above.
I really like the simplicity of this and the “do one thing well” approach. In my GoAWK interpreter it would be very helpful – I have about 100 if err != nil { return nil }
constructs that it would simplify and tidy up, and that’s in a fairly small codebase.
I like this proposal also.
…
I do think that try
as-proposed makes some code significantly nicer. Here’s a function I chose more or less at random from my current project’s code base, with some of the names changed. I am particularly impressed by how try
works when assigning to struct fields. (That is assuming my reading of the proposal is correct, and that this works?)
The existing code:
func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
err := dbfile.RunMigrations(db, dbMigrations)
if err != nil {
return nil, err
}
t := &Thing{
thingy: thingy,
}
t.scanner, err = newScanner(thingy, db, client)
if err != nil {
return nil, err
}
t.initOtherThing()
return t, nil
}
With try
:
func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
try(dbfile.RunMigrations(db, dbMigrations))
t := &Thing{
thingy: thingy,
scanner: try(newScanner(thingy, db, client)),
}
t.initOtherThing()
return t, nil
}
No loss of readability, except perhaps that it’s less obvious that newScanner
might fail. But then in a world with try
Go programmers would be more sensitive to its presence.
@adg Yes, try
can be used as you’re using it in your example. I let your comments re: named returns stand as is.
…
In the example @adg gave, there are two potential failures but no context. If newScanner
and RunMigrations
don’t themselves provide messages that clue you into which one went wrong, then you’re left guessing.
In the example @adg gave, there are two potential failures but no context. If newScanner and RunMigrations don’t themselves provide messages that clue you into which one went wrong, then you’re left guessing.
That’s right, and that’s the design choice we made in this particular piece of code. We do wrap errors a lot in other parts of the code.
After the complexities of the check/handle
draft design, I was pleasantly surprised to see this much simpler and pragmatic proposal land though I’m disappointed that there has been so much push-back against it.
Admittedly a lot of the push-back is coming from people who are quite happy with the present verbosity (a perfectly reasonable position to take) and who presumably wouldn’t really welcome any proposal to alleviate it. For the rest of us, I think this proposal hits the sweet spot of being simple and Go-like, not trying to do too much and dove-tailing well with the existing error handling techniques on which you could always fall back if try
didn’t do exactly what you wanted.
Regarding some specific points:
My initial reaction to this was a 👎 as I imagined that handling several error prone calls within a function would make the defer
error handle confusing. After reading through the whole proposal, I have flipped my reaction to a ❤️ and 👍 as I learnt that this can still be achieved with relatively low complexity.
I haven’t commented on the error handling proposals so far because i’m generally in favour, and i like the way they’re heading. Both the try function defined in the proposal and the try statement proposed by @thepudds seem like they would be reasonable additions to the language. I’m confident that whatever the Go team comes up with will be a good.
The more I think, I like the current proposal, as is.
If we need error handling, we always have the if statement.
Thanks everybody for the prolific feedback so far; this is very informative. Here’s my attempt at an initial summary, to get a better feeling for the feedback. Apologies in advance for anybody I have missed or misrepresented; I hope that I got the overall gist of it right.
0) On the positive side, @rasky, @adg, @eandre, @dpinela, and others explicitly expressed happiness over the code simplification that try
provides.
1) The most important concern appears to be that try
does not encourage good error handling style but instead promotes the “quick exit”. (@agnivade, @peterbourgon, @politician, @a8m, @eandre, @prologic, @kungfusheep, @cpuguy, and others have voiced their concern about this.)
2) Many people don’t like the idea of a built-in, or the function syntax that comes with it because it hides a return
. It would be better to use a keyword. (@sheerun, @Redundancy, @dolmen, @komuw, @RobertGrantEllis, @elagergren-spideroak). try
may also be easily overlooked (@peterbourgon), especially because it can appear in expressions that may be arbitrarily nested. @natefinch is concerned that try
makes it “too easy to dump too much in one line”, something that we usually try to avoid in Go. Also, IDE support to emphasize try
may not be sufficient (@dominikh); try
needs to “stand on its own”.
3) For some, the status quo of explicit if
statements is not a problem, they are happy with it (@bitfield, @marwan-at-work, @natefinch). It’s better to have only one way to do things (@gbbr); and explicit if
statements are better than implicit return
’s (@DavexPro, @hmage, @prologic, @natefinch).
Along the same lines, @mattn is concerned about the “implicit binding” of the error result to try
- the connection is not explicitly visible in the code.
4) Using try
will make it harder to debug code; for instance, it may be necessary to rewrite a try
expression back into an if
statement just so that debugging statements can be inserted (@deanveloper, @typeless, @networkimprov, others).
5) There’s some concern about the use of named returns (@buchanae, @adg).
Several people have provided suggestions to improve or modify the proposal:
6) Some have picked up on the idea of an optional error handler (@beoran) or format string provided to try
(@unexge, @a8m, @eandre, @gotwarlost) to encourage good error handling.
7) @pierrec suggested that gofmt
could format try
expressions suitably to make them more visible.
Alternatively, one could make existing code more compact by allowing gofmt
to format if
statements checking for errors on one line (@zeebo).
8) @marwan-at-work argues that try
simply shifts error handling from if
statements to try
expressions. Instead, if we want to actually solve the problem, Go should “own” error handling by making it truly implicit. The goal should be to make (proper) error handling simpler and developers more productive (@cpuguy).
9) Finally, some people don’t like the name try
(@beoran, @HiImJC, @dolmen) or would prefer a symbol such as ?
(@twisted1919, @leaxoy, others).
Some comments on this feedback (numbered accordingly):
0) Thanks for the positive feedback! :-)
1) It would be good to learn more about this concern. The current coding style using if
statements to test for errors is about as explicit as it can be. It’s very easy to add additional information to an error, on an individual basis (for each if
). Often it makes sense to handle all errors detected in a function in a uniform way, which can be done with a defer
- this is already possible now. It is the fact that we already have all the tools for good error handling in the language, and the problem of a handler construct not being orthogonal to defer
, that led us to leave away a new mechanism solely for augmenting errors.
2) There is of course the possibility to use a keyword or special syntax instead of a built-in. A new keyword will not be backward-compatible. A new operator might, but seems even less visible. The detailed proposal discusses the various pros and cons at length. But perhaps we are misjudging this.
3) The reason for this proposal is that error handling (specifically the associated boilerplate code) was mentioned as a significant issue in Go (next to the lack of generics) by the Go community. This proposal directly addresses the boilerplate concern. It does not do more than solve the most basic case because any more complex case is better handled with what we already have. So while a good number of people are happy with the status quo, there is a (probably) equally large contingent of people that would love a more streamlined approach such as try
, well-knowing that this is “just” syntactic sugar.
4) The debugging point is a valid concern. If there’s a need to add code between detecting an error and a return
, having to rewrite atry
expression into an if
statement could be annoying.
5) Named return values: The detailed document discusses this at length. If this is the main concern about this proposal then we’re in a good spot, I think.
6) Optional handler argument to try
: The detailed document discusses this as well. See the section on Design iterations.
7) Using gofmt
to format try
expressions such that they are extra visible would certainly be an option. But it would take away from some of the benefits of try
when used in an expression.
8) We have considered looking at the problem from the error handling (handle
) point of view rather than from the error testing (try
) point of view. Specifically, we briefly considered only introducing the notion of an error handler (similar to the original design draft presented at last year’s Gophercon). The thinking was that if (and only if) a handler is declared, in multi-value assignments where the last value is of type error
, that value can simply be left away in an assignment. The compiler would implicitly check if it is non-nil, and if so branch to the handler. That would make explicit error handling disappear completely and encourage everybody to write a handler instead. This seemed to extreme an approach because it would be completely implicit - the fact that a check happens would be invisible.
9) May I suggest that we don’t bike-shed the name at this point. Once all the other concerns are settled is a better time to fine-tune the name.
This is not to say that the concerns are not valid - the replies above are simply stating our current thinking. Going forward, it would be good to comment on new concerns (or new evidence in support of these concerns) - just restating what has been said already does not provide us with more information.
And finally, it appears that not everybody commenting on the issue has read the detailed doc. Please do so before commenting to avoid repeating what has already been said. Thanks.
Thank you very much @griesemer for taking the time to go through everyone’s ideas and explicitly providing thoughts. I think that it really helps with the perception that the community is being heard in the process.
@griesemer thank you for the incredible work going through all the comments and answering most if not all of the feedback 🎉
Thanks again to everybody for all the new comments; it’s a significant time investment to keep up with the discussion and write up extensive feedback. And better even, despite the sometimes passionate arguments, this has been a rather civil thread so far. Thanks!
Here’s another quick summary, this time a bit more condensed; apologies to those I didn’t mention, forgot, or misrepesented. At this point I think some larger themes are emerging:
1) In general, using a built-in for the try
functionality is felt to be a bad choice: Given that it affects control flow it should be at least a keyword (@carloslenz “prefers making it a statement without parenthesis”); try
as an expression seems not a good idea, it harms readability (@ChrisHines, @jimmyfrasche), they are “returns without a return
”. @brynbellomy did an actual analysis of try
used as identifiers; there appear to be very few percentage-wise, so it might be possible to go the keyword route w/o affecting too much code.
2) @crawshaw took some time to analyze a couple hundred use cases from the std library and came to the conclusion that try
as proposed almost always improved readability. @jimmyfrasche came to the opposite conclusion.
3) Another theme is that using defer
for error decoration is not ideal. @josharian points out the defer
’s always run upon function return, but if they are here for error decoration, we only care about their body if there’s an error, which could be a source of confusion.
4) Many wrote up suggestions for improving the proposal. @zeebo, @patrick-nyt are in supoort of gofmt
formatting simple if
statements on a single line (and be happy with the status quo). @jargv suggested that try()
(without arguments) could return a pointer to the currently “pending” error, which would remove the need to name the error result just so one has access to it in a defer
; @masterada suggested using errorfunc()
instead. @velovix revived the idea of a 2-argument try
where the 2nd argument would be an error handler.
@klaidliadon, @networkimprov are in favor of special “assignment operators” such as in f, # := os.Open()
instead of try
. @networkimprov filed a more comprehensive alternative proposal investigating such approaches (see issue #32500). @mikeschinkel also filed an alternative proposal suggesting to introduce two new general purpose language features that could be used for error handling as well, rather than an error-specific try
(see issue #32473). @josharian revived a possibility we discussed at GopherCon last year where try
doesn’t return upon an error but instead jumps (with a goto
) to a label named error
(alternatively, try
might take the name of a target label).
5) On the subject of try
as a keyword, two lines of thoughts have appeared. @brynbellomy suggested a version that might alternatively specify a handler:
a, b := try f()
a, b := try f() else err { /* handle error */ }
@thepudds goes a step further and suggests try
at the beginning of the line, giving try
the same visibility as a return
:
try a, b := f()
Both of these could work with defer
.
It would be helpful if this could be accompanied (at some stage of accepted-ness) by a tool to transform Go code to use try
in some subset of error-returning functions where such a transformation can be easily performed without changing semantics. Three benefits occur to me:
try
could be used in their codebase.try
lands in a future version of Go, people will likely want to change their code to make use of it. Having a tool to automate the easy cases will help a lot.try
will make it easy to examine the effects of the implementation at scale. (Correctness, performance, and code size, say.) The implementation may be simple enough to make this a negligible consideration, though.Someone has already implemented this 5 years ago. If you are interested, you can try this feature
https://news.ycombinator.com/item?id=20101417
I implemented try() in Go five years ago with an AST preprocessor and used it in real projects, it was pretty nice: https://github.com/lunixbochs/og
Here are some examples of me using it in error-check-heavy functions: https://github.com/lunixbochs/poxd/blob/master/tls.go#L13
@s4n-gt Thanks for this link. I was not aware of it.
@cespare: It should be possible for somebody to write a go fix
that rewrites existing code suitable for try
such that it uses try
. It may be useful to get a feel for how existing code could be simplified. We don’t expect any significant changes in code size or performance, since try
is just syntactic sugar, replacing a common pattern by a shorter piece of source code that produces essentially the same output code. Note also that code that uses try
will be bound to use a Go version that’s at least the version at which try
was introduced.
Would it be worth analyzing openly available Go code for error checking statements to try and figure out if most error checks are truly repetitive or if in most cases, multiple checks within the same function add different contextual information? The proposal would make a lot of sense for the former case but wouldn’t help the latter. In the latter case, people will either continue using if err != nil
or give up on adding extra context, use try()
and resort to adding common error context per function which IMO would be harmful. With upcoming error values features, I think we expect people wrap errors with more info more often. Probably I misunderstood the proposal but AFAIU, this helps reduce the boilerplate only when all errors from a single function must be wrapped in exactly one way and doesn’t help if a function deals with five errors that might need to be wrapped differently. Not sure how common such cases in the wild (pretty common in most of my projects) are but I’m concerned try()
might encourage people to use common wrappers per function even when it’d make sense to wrap different errors differently.
Just a quick comment backed with data of a small sample set:
We propose a new built-in function called try, designed specifically to eliminate the boilerplate if statements typically associated with error handling in Go
Iff this is the core problem being solved by this proposal, I find that this “boilerplate” only accounts for ~1.4% of my code across dozens of publically available open source projects totalling ~60k SLOC.
Curious if anyone else has similar stats?
On a much larger codebase like Go itself totalling around ~1.6M SLOC this amounts to about ~0.5% of the codebase having lines like if err != nil
.
Is this really the most impactful problem to solve with Go 2?
I have briefed through some very often used packages, looking for go code that “suffers” from err handling but must have been well thought before written, trying to figure out what “magic” would the proposed try() would do. Currently, unless I misunderstood the proposal, many of those (e.g. not super basic error handling) would not gain much, or would have to stay with the “old” error handling style. Example from net/http/request.go:
func (r *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error) {
`
trace := httptrace.ContextClientTrace(r.Context())
if trace != nil && trace.WroteRequest != nil {
defer func() {
trace.WroteRequest(httptrace.WroteRequestInfo{
Err: err,
})
}()
}
// Find the target host. Prefer the Host: header, but if that
// is not given, use the host from the request URL.
//
// Clean the host, in case it arrives with unexpected stuff in it.
host := cleanHost(r.Host)
if host == "" {
if r.URL == nil {
return errMissingHost
}
host = cleanHost(r.URL.Host)
}
// According to RFC 6874, an HTTP client, proxy, or other
// intermediary must remove any IPv6 zone identifier attached
// to an outgoing URI.
host = removeZone(host)
ruri := r.URL.RequestURI()
if usingProxy && r.URL.Scheme != "" && r.URL.Opaque == "" {
ruri = r.URL.Scheme + "://" + host + ruri
} else if r.Method == "CONNECT" && r.URL.Path == "" {
// CONNECT requests normally give just the host and port, not a full URL.
ruri = host
if r.URL.Opaque != "" {
ruri = r.URL.Opaque
}
}
if stringContainsCTLByte(ruri) {
return errors.New("net/http: can't write control character in Request.URL")
}
// TODO: validate r.Method too? At least it's less likely to
// come from an attacker (more likely to be a constant in
// code).
// Wrap the writer in a bufio Writer if it's not already buffered.
// Don't always call NewWriter, as that forces a bytes.Buffer
// and other small bufio Writers to have a minimum 4k buffer
// size.
var bw *bufio.Writer
if _, ok := w.(io.ByteWriter); !ok {
bw = bufio.NewWriter(w)
w = bw
}
_, err = fmt.Fprintf(w, "%s %s HTTP/1.1\r\n", valueOrDefault(r.Method, "GET"), ruri)
if err != nil {
return err
}
// Header lines
_, err = fmt.Fprintf(w, "Host: %s\r\n", host)
if err != nil {
return err
}
if trace != nil && trace.WroteHeaderField != nil {
trace.WroteHeaderField("Host", []string{host})
}
// Use the defaultUserAgent unless the Header contains one, which
// may be blank to not send the header.
userAgent := defaultUserAgent
if r.Header.has("User-Agent") {
userAgent = r.Header.Get("User-Agent")
}
if userAgent != "" {
_, err = fmt.Fprintf(w, "User-Agent: %s\r\n", userAgent)
if err != nil {
return err
}
if trace != nil && trace.WroteHeaderField != nil {
trace.WroteHeaderField("User-Agent", []string{userAgent})
}
}
// Process Body,ContentLength,Close,Trailer
tw, err := newTransferWriter(r)
if err != nil {
return err
}
err = tw.writeHeader(w, trace)
if err != nil {
return err
}
err = r.Header.writeSubset(w, reqWriteExcludeHeader, trace)
if err != nil {
return err
}
if extraHeaders != nil {
err = extraHeaders.write(w, trace)
if err != nil {
return err
}
}
_, err = io.WriteString(w, "\r\n")
if err != nil {
return err
}
if trace != nil && trace.WroteHeaders != nil {
trace.WroteHeaders()
}
// Flush and wait for 100-continue if expected.
if waitForContinue != nil {
if bw, ok := w.(*bufio.Writer); ok {
err = bw.Flush()
if err != nil {
return err
}
}
if trace != nil && trace.Wait100Continue != nil {
trace.Wait100Continue()
}
if !waitForContinue() {
r.closeBody()
return nil
}
}
if bw, ok := w.(*bufio.Writer); ok && tw.FlushHeaders {
if err := bw.Flush(); err != nil {
return err
}
}
// Write body and trailer
err = tw.writeBody(w)
if err != nil {
if tw.bodyReadError == err {
err = requestBodyReadError{err}
}
return err
}
if bw != nil {
return bw.Flush()
}
return nil
} `
or as used in a thorough test such as pprof/profile/profile_test.go: ` func checkAggregation(prof *Profile, a *aggTest) error { // Check that the total number of samples for the rows was preserved. total := int64(0)
samples := make(map[string]bool)
for _, sample := range prof.Sample {
tb := locationHash(sample)
samples[tb] = true
total += sample.Value[0]
}
if total != totalSamples {
return fmt.Errorf("sample total %d, want %d", total, totalSamples)
}
// Check the number of unique sample locations
if a.rows != len(samples) {
return fmt.Errorf("number of samples %d, want %d", len(samples), a.rows)
}
// Check that all mappings have the right detail flags.
for _, m := range prof.Mapping {
if m.HasFunctions != a.function {
return fmt.Errorf("unexpected mapping.HasFunctions %v, want %v", m.HasFunctions, a.function)
}
if m.HasFilenames != a.fileline {
return fmt.Errorf("unexpected mapping.HasFilenames %v, want %v", m.HasFilenames, a.fileline)
}
if m.HasLineNumbers != a.fileline {
return fmt.Errorf("unexpected mapping.HasLineNumbers %v, want %v", m.HasLineNumbers, a.fileline)
}
if m.HasInlineFrames != a.inlineFrame {
return fmt.Errorf("unexpected mapping.HasInlineFrames %v, want %v", m.HasInlineFrames, a.inlineFrame)
}
}
// Check that aggregation has removed finer resolution data.
for _, l := range prof.Location {
if !a.inlineFrame && len(l.Line) > 1 {
return fmt.Errorf("found %d lines on location %d, want 1", len(l.Line), l.ID)
}
for _, ln := range l.Line {
if !a.fileline && (ln.Function.Filename != "" || ln.Line != 0) {
return fmt.Errorf("found line %s:%d on location %d, want :0",
ln.Function.Filename, ln.Line, l.ID)
}
if !a.function && (ln.Function.Name != "") {
return fmt.Errorf(`found file %s location %d, want ""`,
ln.Function.Name, l.ID)
}
}
}
return nil
} ` These are two examples I can think of in which one would say : “I would like a better error handling option”
Can someone demonstrate how would these improve using try() ?
…
As @prologic mentioned, is this try()
proposal predicated on a large percentage of code that would use this use-case, or is it instead based on attempting to placate those who have complained about Go error handling?
I wish I knew how to give you stats from my code base without exhaustively reviewing every file and take notes; I don’t know how @prologic was able to though glad he did.
But anecdotally I would be surprised if try()
addressed 5% of my use-cases and would suspect that it would address less than 1%. Do you know for certain that others have vastly different results? Have you taken a subset of the standard library and tried to see how it would be applied?
I actually tried to implement this idea as Go translator about half a year ago. I don’t have strong opinion whether this feature should be added as Go builtin, but let me share the experience (though I’m not sure it’s useful).
https://github.com/rhysd/trygo
I called the extended language TryGo and implemented TryGo to Go translator.
With the translator, the code
func CreateFileInSubdir(subdir, filename string, content []byte) error {
cwd := try(os.Getwd())
try(os.Mkdir(filepath.Join(cwd, subdir)))
p := filepath.Join(cwd, subdir, filename)
f := try(os.Create(p))
defer f.Close()
try(f.Write(content))
fmt.Println("Created:", p)
return nil
}
can be translated into
func CreateFileInSubdir(subdir, filename string, content []byte) error {
cwd, _err0 := os.Getwd()
if _err0 != nil {
return _err0
}
if _err1 := os.Mkdir(filepath.Join(cwd, subdir)); _err1 != nil {
return _err1
}
p := filepath.Join(cwd, subdir, filename)
f, _err2 := os.Create(p)
if _err2 != nil {
return _err2
}
defer f.Close()
if _, _err3 := f.Write(content); _err3 != nil {
return _err3
}
fmt.Println("Created:", p)
return nil
}
For the restriction of language, I could not implement generic try()
call. It is restricted to
but I could try this with my small project. My experience was
err
since its function’s return value is determined by both assignment and try()
special function. very confusingtry()
function lacked ‘wrapping error’ feature as discussed above.I’ve written a little tool: tryhard
(which doesn’t try very hard at the moment) operates on a file-by-file basis and uses simple AST pattern matching to recognize potential candidates for try
and to report (and rewrite) them. The tool is primitive (no type checking) and there’s a decent chance for false positives, depending on prevalent coding style. Read the documentation for details.
Applying it to $GOROOT/src
at tip reports > 5000 (!) opportunities for try
. There may be plenty of false positives, but checking out a decent sample by hand suggests that most opportunities are real.
Using the rewrite feature shows how the code will look like using try
. Again, a cursory glance at the output shows significant improvement in my mind.
(Caution: The rewrite feature will destroy files! Use at your own risk.)
Hopefully this will provide some concrete insight into what code might look like using try
and lets us move past idle and unproductive speculation.
Thanks & enjoy.
Here’s an interesting tryhard
false negative, from github.com/josharian/pct
. I mention it here because:
try
detection is trickyif err != nil
impacts how people (me at least) structure their code, and that try
can help with thatBefore:
var err error
switch {
case *flagCumulative:
_, err = fmt.Fprintf(w, "% 6.2f%% % 6.2f%%% 6d %s\n", p, f*float64(runtot), line.n, line.s)
case *flagQuiet:
_, err = fmt.Fprintln(w, line.s)
default:
_, err = fmt.Fprintf(w, "% 6.2f%%% 6d %s\n", p, line.n, line.s)
}
if err != nil {
return err
}
After (manual rewrite):
switch {
case *flagCumulative:
try(fmt.Fprintf(w, "% 6.2f%% % 6.2f%%% 6d %s\n", p, f*float64(runtot), line.n, line.s))
case *flagQuiet:
try(fmt.Fprintln(w, line.s))
default:
try(fmt.Fprintf(w, "% 6.2f%%% 6d %s\n", p, line.n, line.s))
}
Change https://golang.org/cl/182717 mentions this issue: src: apply tryhard -r $GOROOT/src
For a visual idea of try
in the std library, head over to CL 182717.
Thanks, @josharian, for this. Yes, it may impossible even for a good tool to detect all possible use candidates for try
. But luckily that is not the primary goal (of this proposal). Having a tool is useful, but I see the main benefit of try
in code that’s not yet written (because there’s going to be much more of that than code that we already have).
The standard library ends up not being representative of “real” Go code in that it doesn’t spend much time coordinating or connecting other packages. We’ve noticed this in the past as the reason why there is so little channel usage in the standard library compared to packages farther up the dependency food chain. I suspect error handling and propagation ends up being similar to channels in this respect: you’ll find more the higher up you go.
For this reason, it would be interesting for someone to run tryhard on some larger application code bases and see what fun things can be discovered in that context. (The standard library is interesting too, but as more of a microcosm than an accurate sampling of the world.)
@griesemer While trying tryhard on a file with 97 err’s none caught, I found that the 2 patterns not translated 1 :
if err := updateItem(tx, fields, entityView.DataBinding, entityInstance); err != nil {
tx.Rollback()
return nil, err
}
Is not replaced, probably because the tx.Rollback() between err := and the return line, Which I assume can only should be handled by defer - and if all paths of error needs to tx.Rollback() Is this right ?
It also does not suggest:
if err := db.Error; err != nil {
return nil, err
} else if itemDb, err := GetItem(c, entity, entityView, ItemRequest{recNo}); err != nil {
return nil, err
} else {
return itemDb, nil
}
or
if err := db.Error; err != nil {
return nil, err
} else {
if itemDb, err := GetItem(c, entity, entityView, ItemRequest{recNo}); err != nil {
return nil, err
} else {
return itemDb, nil
}
return result, nil
}
Is this because of the shadowing or the nesting try would translate to ? meaning - should this use try or suggested to be left as err := … return err ?
@guybrand Re: the two patterns you found:
1) yes, tryhard
doesn’t try very hard. type-checking is necessary for more complex cases. If tx.Rollback()
should be done in all paths, defer
might be the right approach. Otherwise, keeping the if
might be the right approach. It depends on the specific code.
2) Same here: tryhard
doesn’t look for this more complex pattern. Maybe it could.
Again, this is an experimental tool to get some quick answers. Doing it right requires a bit more work.
@griesemer , sorry if I missed a more specific request for a format, but I would love to share some results, and have access (a possible permission) to some companies’ source code. Below is a real example for a small project, I think the attached results gives a good sample, if so, we can probably share some table with similar results:
Total = Number of code lines
$find /path/to/repo -name '*.go' -exec cat {} \; | wc -l
Errs = number of lines with err := (this probably misses err = , and myerr := , but I think in most cases it covers)
$find /path/to/repo -name '*.go' -exec cat {} \; | grep "err :=" | wc -l
tryhard = number of lines tryhard found
the first case I tested to study returned: Total = 5106 Errs = 111 tryhard = 16
bigger code base Total = 131777 Errs = 3289 tryhard = 265
If this format is acceptable, let us know how you want to get the results, I assume just throwing it here would not be the correct format Also, it would probably be a quickie to have tryhard count the lines, occasions of err := (and probably err = , only 4 on the code base I tried to learn upon)
Thanks.
From @griesemer in https://github.com/golang/go/issues/32437#issuecomment-503276339
I urge you to look at this code for an example.
In regards to that code, I noticed that the out file created here never seems to be closed. Additionally, it’s important to check errors from closing files you’ve written to, because that may be the only time you’re informed that there was a problem with a write.
I’m bringing this up not as a bug report (though maybe it should be?), but as a chance to see if try
has an effect on how one might fix it. I’ll enumerate all of the ways that I can think of to fix it and consider if the addition of try
would help or hurt. Here are some ways:
outf.Close()
right before any error return.func foo() (err error) {
outf := try(os.Create())
defer func() {
cerr := outf.Close()
if err == nil {
err = cerr
}
}()
...
}
defer outf.Close()
to ensure resource cleanup, and try(outf.Close())
before returning to ensure no errors.func foo() error {
outf := try(os.Create())
if err := helper(outf); err != nil {
outf.Close()
return err
}
try(outf.Close())
return nil
}
I think in all cases except case number 1, try
is at worst neutral and usually positive. And I’d consider number 1 to be the least palatable option given the size and number of error possibilities in that function, so adding try
would reduce the appeal of a negative choice.
I hope this analysis was useful.
@guybrand Thanks for these numbers. It would be good to have some insights as to why the tryhard
numbers are what they are. Perhaps there’s a lot of specific error decoration going on? If so, that’s great and if
statements are the right choice.
I’ll improve the tool when I get to it.
@guybrand, tryhard numbers are great but even better would be descriptions of why specific examples did not convert and furthermore would have been inappropriate to rewrite to be possible to convert. @tv42’s example and explanation is an instance of this.
some data:
out of a ~70k LOC project which I’ve hawked over to eliminate “naked err returns” religiously, we still have 612 naked error returns. mostly dealing with a case where an error is logged, but the message is only important internally (the message to the user is predefined). try() will have a bigger saving than just two lines per each naked return though, because with predefined errors we can defer a handler and use try in more places.
more interestingly, in the vendor directory, out of ~620k+ LOC, we have only 1600 naked error returns. libraries we choose tend to decorate errors even more religiously than we do.
@guybrand (and @griesemer) with regard to your second unrecognized pattern, see https://github.com/griesemer/tryhard/issues/2
For people trying out tryhard
, if you haven’t already, I would encourage you not only to look at what changes the tool made, but also to grep for remaining instances of err != nil
and look at what it left alone, and why.
(And also note that there are a couple of issues and PRs at https://github.com/griesemer/tryhard/.)
OK, numbers and data it is then. :)
I ran tryhard on the sources several services of our microservice platform, and compared it with the results of loccount and grep ‘if err’. I got the following results in the order loccount / grep ‘if err’ | wc / tryhard:
1382 / 64 / 14 108554 / 66 / 5 58401 / 22 / 5 2052/247/39 12024 / 1655 / 1
Some of our microservices do a lot of error handling and some do only little, but unfortunately, tryhard was only able to automatically improve the code, in at best, 22% of the cases, at worse less than 1%. Now, we are not going to manually rewrite our error handling so a tool like tryhard will be essential to introduce try()
in our codebase. I appreciate that this is a simple preliminary tool, but I was surprised at how rarely it was able to help.
But I think that now, with number in hand, I can say that for our use, try() is not really solving any problem, or, at least not until tryhard becomes much better.
I also found in our code bases that the if err != nil { return err }
use case of try()
is actually very rare, unlike in the go compiler, where it is common. With all due respect, but I think that the Go designers, who are looking at the Go compiler source code far more often than at other code bases, are overestimating the usefulness of try()
because of this.
@rsc , @griesemer
As for examples, I gave two repeating samples here which tryHard missed, one will probably stay as “if Err :=“, the other may be resolved
as for error decoration, two recurring patterns I see in the code are (I put the two in one code snippet):
if v, err := someFunction(vars...) ; err != nil {
return fmt.Errorf("extra data to help with where did error occur and params are %s , %d , err : %v",
strParam, intParam, err)
} else if v2, err := passToAnotherFunc(v,vars ...);err != nil {
extraData := DoSomethingAccordingTo(v2,err)
return formatError(err,extraData)
} else {
}
And many the times the formatError is some standard for the app or ever cross repos, most repeating is DbError formatting (one function in all the app/apps, used in dozens of locations), in some cases (without going into “is this a correct pattern”) saving some data to log (failing sql query you would not like to pass up the stack) and some other text to the error.
In other words, if I want to “do anything smart with extra data such as logging error A and raising error B, on top of my mention of these two options to extend error handling This is another option to “more than just return the error and let ‘someone else’ or ‘some other func’ handle it”
Which means there is probably more usage for try() in “libraries” than in “executable programs”, perhaps I will try to run the Total/Errs/tryHard comparison differentiating libs from runnables (“apps”).
@beoran tryhard
is very rudimentary at the moment. Do you have a sense for the most common reasons why try
would be rare in your codebase? E.g. because you decorate the errors? Because you do other extra work before returning? Something else?
@josharian I cannot divulge too much here, however, the reasons are quite diverse. As you say, we do decorate the errors, and or also do different processing, and also, an important use case is that we log them, where the log message differs for each error that a function can return, or because we use the if err := foo() ; err != nil { /* various handling*/ ; return err }
form, or other reasons.
What I want to stress is this: the simple use case for which try()
is designed occur only very rarely in our code base. So, for us there is not much to be gained to adding ‘try()’ to the language.
EDIT: If try() is going to be implemented then I think the next step should be to make tryhard much better, so it can be used widely to upgrade existing code bases.
The tools tryhard
is very informative !
I could see that i use often return ...,err
, but only when i know that i call a function that already wrap the error (with pkg/errors
), mostly in http handlers. I win in readability with fewer line of code.
Then in theses http handler i would add a defer fmt.HandleErrorf(&err, "handler xyz")
and finally add more context than before.
I see also lot of case where i don’t care of the error at all fmt.Printf
and i will do it with try
.
Will it be possible for example to do defer try(f.Close())
?
So, maybe try
will finally help to add context and push best practice rather than the opposite.
I’m very impatient to test in real !
@flibustenet The proposal as is won’t allow defer try(f())
(see the rationale). There’s all kinds of problems with that.
When using this tryhard
tool to see changes in a codebase, could we also compare the ratio of if err != nil
before and after to see whether it’s more common to add context or to just pass the error back?
My thinking is that maybe a hypothetical huge project can see 1000 places where try()
was added but there are 10000 if err != nil
that add context so even though 1000 looks huge, it’s only 10% of the full thing.
@Goodwine Yes. I probably won’t get to make this change this week, but the code is pretty straight-forward and self-contained. Feel free to give it a try (no pun intended), clone, and adjust as needed.
To clarify:
Does
func f() (n int, err error) {
n = 7
try(errors.New("x"))
// ...
}
return (0, “x”) or (7, “x”)? I’d assume the latter.
Does the error return have to be named in the case where there’s no decoration or handling (like in an internal helper function)? I’d assume not.
Your example returns 7, errors.New("x")
. This should be clear in the full doc that will soon be submitted (https://golang.org/cl/180557).
The error result parameter does not need to be named in order to use try
. It only needs to be named if the function needs to refer to it in a deferred function or elsewhere.
Not sure why anyone ever would write a function like this but what would be the envisioned output for
try(foobar())
If foobar
returned (error, error)
@webermaster Only the last error
result is special for the expression passed to try
, as described in the proposal doc.
…
I didn’t see how to answer this question from the design doc. What does this code do:
func foo() (err error) {
src := try(getReader())
if src != nil {
n, err := src.Read(nil)
if err == io.EOF {
return nil
}
try(err)
println(n)
}
return nil
}
My understanding is that it would desugar into
func foo() (err error) {
tsrc, te := getReader()
if err != nil {
err = te
return
}
src := tsrc
if src != nil {
n, err := src.Read(nil)
if err == io.EOF {
return nil
}
terr := err
if terr != nil {
err = terr
return
}
println(n)
}
return nil
}
which fails to compile because err
is shadowed during a naked return. Would this not compile? If so, that’s a very subtle failure, and doesn’t seem too unlikely to happen. If not, then more is going on than some sugar.
…
Regarding your question about the “rewrite”: No, there wouldn’t be a compilation error. Note that the rewrite happens internally (and may be more efficient than the code pattern suggest), and there is no need for the compiler to complain about a shadowed return. In your example, you declared a local err
variable in a nested scope. try
would still have direct access to the result err
variable, of course. The rewrite might look more like this under the covers.
[edited] PS: A better answer would be: try
is not a naked return (even though the rewrite looks like it). After all one explicitly gives try
an argument that contains (or is) the error that is returned if not nil
. The shadow error for naked returns is an error on the source (not the underlying translation of the source. The compiler doesn’t need the error.
@griesemer Thanks for the clarification about the rewrite. I’m glad that it will compile.
…
When studying the proposal’s code I find that the behaviour is non-obvious and somewhat hard to reason about.
When I see try()
wrapping an expression, what will happen if an error is returned?
Will the error just be ignored? Or will it jump to the first or the most recent defer
, and if so will it automatically set a variable named err
inside the closure that, or will it pass it as a parameter (I don’t see a parameter?). And if not an automatic error name, how do I name it? And does that mean I can’t declare my own err
variable in my function, to avoid clashes?
And will it call all defer
s? In reverse order or regular order?
Or will it return from both the closure and the func
where the error was returned? (Something I would never have considered if I had not read here words that imply that.)
After reading the proposal and all the comments thus far I still honestly do not know the answers to the above questions. Is that the kind of feature we want to add to a language whose advocates champion as being “Captain Obvious?”
@alexbrainman > How would we deal with compile errors when people use go1.13 and before to build code with try?
My understanding is the the version of the language itself will be controlled by the go
language version directive in the go.mod
file for each piece of code being compiled.
The in-flight go.mod
documentation describes the go
language version directive like so:
The expected language version, set by the
go
directive, determines which language features are available when compiling the module. Language features available in that version will be available for use. Language features removed in earlier versions, or added in later versions, will not be available. Note that the language version does not affect build tags, which are determined by the Go release being used.
If hypothetically something like a new try
builtin lands in something like Go 1.15, then at that point someone whose go.mod
file reads go 1.12
would not have access to that new try
builtin even if they compile with the Go 1.15 toolchain. My understanding of the current plan is that they would need to change the Go language version declared in their go.mod
from go 1.12
to instead read go 1.15
if they want to use the new Go 1.15 language feature of try
.
On the other hand, if you have code that uses try
and that code lives in a module whose go.mod
file declares its Go language version as go 1.15
, but then someone attempts to build that with the Go 1.12 toolchain, at that point the Go 1.12 toolchain will fail with a compile error. The Go 1.12 toolchain does not know anything about try
, but it knows enough to print an additional message that the code that failed to compile claimed to require Go 1.15 based on what is in the go.mod
file. You can actually attempt this experiment right now using today’s Go 1.12 toolchain, and see the resulting error message:
.\hello.go:3:16: undefined: try
note: module requires Go 1.15
There is a much longer discussion in the Go2 transitions proposal document.
That said, the exact details of that might be better discussed elsewhere (e.g., perhaps in #30791, or this recent golang-nuts thread).
If hypothetically something like a new
try
builtin lands in something like Go 1.15, then at that point someone whosego.mod
file readsgo 1.12
would not have access
@thepudds thank you for explaining. But I don’t use modules. So your explanation is way over my head.
Alex
@alexbrainman > How would we deal with compile errors when people use go1.13 and before to build code with try?
If try
was to hypothetically land in something like Go 1.15, then the very short answer to your question is that someone using Go 1.13 to build code with try
would see a compile error like this:
.\hello.go:3:16: undefined: try
note: module requires Go 1.15
(At least as far as I understand what’s been stated about the transition proposal).
@thepudds
If
try
was to hypothetically land in something like Go 1.15, then the very short answer to your question is that someone using Go 1.13
Go 1.13 is not even released yet, so I cannot be using it. And, given my project does not use Go modules, I won’t be able to upgrade to Go 1.13. (I believe Go 1.13 will require everyone to use Go modules)
to build code with
try
would see a compile error like this:.\hello.go:3:16: undefined: try note: module requires Go 1.15
(At least as far as I understand what’s been stated about the transition proposal).
That is all hypothetical. It is difficult for me to comment about fictional stuff. And, maybe you like that error, but I find it confusing and unhelpful.
If try is undefined, I would grep for it. And I will find nothing. What should I do then?
And the note: module requires Go 1.15
is the worst help in this situation. Why module
? Why Go 1.15
?
@alexbrainman, I am sorry for the confusion about what happens if older versions of Go build code containing try. The short answer is that it is like any other time we change the language: the old compiler will reject the new code with a mostly unhelpful message (in this case “undefined: try”). The message is unhelpful because the old compiler does not know about the new syntax and cannot really be more helpful. People would at that point probably do a web search for “go undefined try” and find out about the new feature.
In @thepudds’s example, the code using try has a go.mod that contains the line ‘go 1.15’, meaning the module’s author says the code is written against version of the Go language. This serves as a signal to older go commands to suggest after a compilation error that perhaps the unhelpful message is due to having too old a version of Go. This is explicitly an attempt to make the message a bit more helpful without forcing users to resort to web searches. If it helps, good; if not, web searches seem quite effective anyway.
I am preparing a short demonstration to display this discussion during a go meetup taking place tomorrow, and hear some new thoughts, as I believe most participants on this thread (contributors or watchers), are those who are involved more deeply in the language, and most likely “not the average go developer” (just a hunch).
While doing that, I remembered we actually had a meetup about errors and a discussion on two patterns: 1. Extend the error struct while supporting the error interface mystruct.Error() 2. Embed the error either as a field or anonymous field of the struct
type ExtErr struct{
error
someOtherField string
}
These are used in a few stacks my teams actually built.
The proposal Q&A states Q: The last argument passed to try must be of type error. Why is it not sufficient for the incoming argument to be assignable to error? A: “… We can revisit this decision in the future if necessary”
Can anyone comment of similar use cases so we can understand if this need is common for both above error extending options ?
@guybrand, re https://github.com/golang/go/issues/32437#issuecomment-503287670 and with apologies for likely being too late for your meetup:
One problem in general with functions that return not-quite-error types is that for non-interfaces the conversion to error does not preserve nil-ness. So for example if you have your own custom *MyError concrete type (say, a pointer to a struct) and use err == nil as the signal for success, that’s great until you have
func f() (int, *MyError)
func g() (int, error) { x, err := f(); return x, err }
If f returns a nil *MyError, g returns that same value as a non-nil error, which is likely not what was intended. If *MyError is an interface instead of a struct pointer, then the conversion preserves nilness, but even so it’s a subtlety.
For try, you might think that since try would only trigger for non-nil values, no problem. For example, this is actually OK as far as returning a non-nil error when f fails, and it is also OK as far as returning a nil error when f succeeds:
func g() (int, error) {
return try(f()), nil
}
So that’s actually fine, but then you might see this and think to rewrite it to
func g() (int, error) {
return f()
}
which seems like it should be the same but is not.
There are enough other details of the try proposal that need careful examination and evaluation in real experience that it seemed like deciding about this particular subtlety would be best to postpone.
I think this is just sugar, and a small number of vocal opponents teased golang about the repeated use of typing if err != nil ...
and someone took it seriously. I don’t think it’s a problem. The only missing things are these two built-ins:
I retract my previous concerns about control flow and I no longer suggest using ?
. I apologize for the knee-jerk response (though I’d like to point out this wouldn’t have happened had the issue been filed after the full proposal was available).
I disagree with the necessity for simplified error handling, but I’m sure that is a losing battle. try
as laid out in the proposal seems to be the least bad way of doing it.
Like @dominikh, I also disagree with the necessity of simplified error handling.
It moves vertical complexity into horizontal complexity which is rarely a good idea.
If I absolutely had to choose between simplifying error handling proposals, though, this would be my preferred proposal.
It’s a thumbs down from me, principally because the problem it’s aiming to address (“the boilerplate if statements typically associated with error handling”) simply isn’t a problem for me. If all error checks were simply if err != nil { return err }
then I could see some value in adding syntactic sugar for that (though Go is a relatively sugar-free language by inclination).
In fact, what I want to do in the event of a non-nil error varies quite considerably from one situation to the next. Maybe I want to t.Fatal(err)
. Maybe I want to add a decorating message return fmt.Sprintf("oh no: %v", err)
. Maybe I just log the error and continue. Maybe I set an error flag on my SafeWriter object and continue, checking the flag at the end of some sequence of operations. Maybe I need to take some other actions. None of these can be automated with try
. So if the argument for try
is that it will eliminate all if err != nil
blocks, that argument doesn’t stand.
Will it eliminate some of them? Sure. Is that an attractive proposition for me? Meh. I’m genuinely not concerned. To me, if err != nil
is just part of Go, like the curly braces, or defer
. I understand it looks verbose and repetitive to people who are new to Go, but people who are new to Go are not best placed to make dramatic changes to the language, for a whole bunch of reasons.
The bar for significant changes to Go has traditionally been that the proposed change must solve a problem that’s (A) significant, (B) affects a lot of people, and © is well solved by the proposal. I’m not convinced on any of these three criteria. I’m quite happy with Go’s error handling as it is.
One of my favourite things about Go that I generally say when describing the language is that there is only one way to do things, for most things. This proposal goes against that principle a bit by offering multiple ways to do the same thing. I personally think this is not necessary and that it would take away, rather than add to the simplicity and readability of the language.
Couple of things from my perspective. Why are we so concerned about saving a few lines of code? I consider this along the same lines as Small functions considered harmful.
Additionally I find that such a proposal would remove the responsibility of correctly handling the error to some “magic” that I worry will just be abused and encourage laziness resulting in poor quality code and bugs.
The proposal as stated also has a number of unclear behaviors so this is already problematic than an explicit extra ~3 lines that are more clear.
We currently use the defer pattern sparingly in house. There’s an article here which had similarly mixed reception when we wrote it - https://bet365techblog.com/better-error-handling-in-go
However, our usage of it was in anticipation of the check
/handle
proposal progressing.
Check/handle was a much more comprehensive approach to making error handling in go more concise. Its handle
block retained the same function scope as the one it was defined in, whereas any defer
statements are new contexts with an amount, however much, of overhead. This seemed to be more in keeping with go’s idioms, in that if you wanted the behaviour of “just return the error when it happens” you could declare that explicitly as handle { return err }
.
Defer obviously relies on the err reference being maintained also, but we’ve seen problems arise from shadowing the error reference with block scoped vars. So it isn’t fool proof enough to be considered the standard way of handling errors in go.
try
, in this instance, doesn’t appear to solve too much and I share the same fear as others that it would simply lead to lazy implementations, or ones which over-use the defer pattern.
@kungfusheep I like that article. Taking care of wrapping in a defer alone already drives up readability quite a bit without try
.
I’m in the camp that doesn’t feel errors in Go are really a problem. Even so, if err != nil { return err }
can be quite the stutter on some functions. I’ve written functions that needed an error check after almost every statement and none needed any special handling other than wrap and return. Sometimes there just isn’t any clever Buffer struct that’s gonna make things nicer. Sometimes it’s just a different critical step after another and you need to simply short circuit if something went wrong.
Although try
would certainly make that code a lot easier to nicer to read while being fully backwards compatible, I agree that try
isn’t a critical must have feature, so if people are too scared of it maybe it’s best not to have it.
The semantics are quite clear cut though. Anytime you see try
it’s either following the happy path, or it returns. I really can’t get simpler than that.
I am confused about it.
From the blog: Errors are values, from my perspective, it’s designed to be valued not to be ignored.
And I do believe what Rop Pike said, “Values can be programmed, and since errors are values, errors can be programmed.”.
We should not consider error
as exception
, it’s like importing complexity not only for thinking but also for coding if we do so.
“Use the language to simplify your error handling.” – Rob Pike
And more, we can review this slide
I strongly suggest the Go team prioritize generics, as that’s where Go hears the most criticism, and wait on error-handling. Today’s technique is not that painful (tho go fmt
should let it sit on one line).
…
3) For some, the status quo of explicit if
statements is not a problem, they are happy with it (@bitfield, @marwan-at-work, @natefinch). It’s better to have only one way to do things (@gbbr); and explicit if
statements are better than implicit return
’s (@DavexPro, @hmage, @prologic, @natefinch).
Along the same lines, @mattn is concerned about the “implicit binding” of the error result to try
- the connection is not explicitly visible in the code.
…
3) The reason for this proposal is that error handling (specifically the associated boilerplate code) was mentioned as a significant issue in Go (next to the lack of generics) by the Go community. This proposal directly addresses the boilerplate concern. It does not do more than solve the most basic case because any more complex case is better handled with what we already have. So while a good number of people are happy with the status quo, there is a (probably) equally large contingent of people that would love a more streamlined approach such as try
, well-knowing that this is “just” syntactic sugar.
…
One of the key responses from the Go team has been “Again, this proposal does not attempt to solve all error handling situations.” And that is probably the most troubling concern, from a governance perspective.
Does this new complicating change to the language that will require everyone to learn the new concepts really address a compelling number of use-cases?
And is that not the same justification members of the core team have denied numerous feature requests from the community? The following is a direct quote from comment made by a member of the Go team in an archetypical response to a feature request submitted about 2 years ago (I’m not naming the person or the specific feature request because this discussion should not be able the people but instead about the language):
“A new language feature needs compelling use cases. All language features are useful, or nobody would propose them; the question is: are they useful enough to justify complicating the language and requiring everyone to learn the new concepts? What are the compelling use cases here? How will people use these? For example, would people expect to be able to … and if so how would they do that? Does this proposal do more than let you…?” — A core Go team member
Frankly when I have seen those responses I have felt one of two feelings:
But in either case my feelings were/are irrelevant; I understand and agree that part of the reason Go is the language so many of us choose to develop in is because of that jealous guarding of the purity of the language.
And that is why this proposal troubles me so, because the Go core team seems to be digging in on this proposal to the same level of someone who dogmatically wants an esoteric feature that there is no way in hell the Go community will ever tolerate.
(And I truly hope the team will not shoot the messenger and take this as constructive criticism from someone who wants to see Go continue being the best it can be for all of us as I would have to be considered “Persona non grata” by the core team.)
If requiring a compelling set of real-world use-cases is the bar for all community-generated feature proposals, should it not also be the same bar for all feature proposals?
```` if it != “broke” { dontFix(it) }
After reading through everything here, and upon further reflection, I am not sure I see try even as a statement something worth adding.
the rationale for it seems to be reducing error handling boiler plate code. IMHO it “declutters” the code but it doesn’t really remove the complexity; it just obscures it. This doesn’t seem strong enough of a reason. The “go
its name doesn’t reflect its function. In its simplest form, what it does is this: “if a function returns an error, return from the caller with an error” but that is too long :-) At the very least a different name is needed.
with try’s implicit return on error, it feels like Go is sort of reluctantly backing into exception handling. That is if A calls be in a try guard and B calls C in a try guard, and C calls D in a try guard, if D returns an error in effect you have caused a non-local goto. It feels too “magical”.
and yet I believe a better way may be possible. Picking try now will close that option off.
I’d say that the reasons that there are so many reactions and so many mini-proposals is that this is an issue where almost everyone agrees that Go language does need to do something to lessen the boilerplate of error handling, but we do not really agree on how to do it.
This proposal, in essence boils down to a built in “macro” for a very common, yet specific case of boilerplate, much like the built in append()
function. So while it is useful for the particular id err!=nil { return err }
particular use case, that’s also all it does. Since it isn’t very helpful in other cases, nor really generally applicable, I’d say it’s underwhelming. I have the feeling that most Go programmers where expecting a bit more, and so the discussion in this thread keeps on going.
Goals
A few comments here have questioned what it is we are trying to do with the proposal. As a reminder, the Error Handling Problem Statement we published last August says in the “Goals” section:
“For Go 2, we would like to make error checks more lightweight, reducing the amount of Go program text dedicated to error checking. We also want to make it more convenient to write error handling, raising the likelihood that programmers will take the time to do it.
Both error checks and error handling must remain explicit, meaning visible in the program text. We do not want to repeat the pitfalls of exception handling.
Existing code must keep working and remain as valid as it is today. Any changes must interoperate with existing code.”
For more about “the pitfalls of exception handling,” see the discussion in the longer “Problem” section. In particular, the error checks must be clearly attached to what is being checked.
I discussed this point already but it seems relevant - code complexity should scale vertically, not horizontally.
try
as an expression encourages code complexity to scale horizontally by encouraging nested calls. try
as a statement encourages code complexity to scale vertically.
I think the criticism against this proposal is largely due to high expectations that were raised by the previous proposal, which would have been a lot more comprehensive. However, I think such high expectations were justified for reasons of consistency. I think what many people would have liked to see, is a single, comprehensive construct for error handling that is useful in all use cases.
Compare this feature, for instance, with the built in append()
function. Append was created because appending to slice was a very common use case, and while it was possible to do it manually it was also easy to do it wrong. Now append()
allows to append not just one, but many elements, or even a whole slice, and it even allows to append a string to a []byte slice. It is powerful enough to cover all use cases of appending to a slice. And hence, no one appends slices manually anymore.
However, try()
is different. It is not powerful enough so we can use it in all cases of error handling. And I think that’s the most serious flaw of this proposal. The try()
builtin function is only really useful, in the sense that it reduces boilerplate, in the most simple of cases, namely just passing on an error to the caller, and with a defer statement, if all errors of the function need to be handled in the same way.
For more complex error handling, we will still need to use if err != nil {}
. This then leads to two distinct styles for error handling, where before there was only one. If this proposal is all we get to help with error handling in Go, then, I think it would be better to do nothing and keep handling error handling with if
like we always have, because at least, this is consistent and had the benefit of “there is only one way to do it”.
@beoran i believe this proposal make further improvement possibles. Like a decorator at last argument of try(..., func(err) error)
, or a tryf(..., "context of my error: %w")
?
@flibustenet While such later extensions could be possible, the proposal as it is now seems to discourage such extensions, mostly because adding an error handler would be redundant with defer.
I guess the difficult problem is how to have comprehensive error handling without duplicating the functionality of defe. Perhaps the defer statement itself could enhanced somehow to allow easier error handling in more complex cases… But, that is a different issue.
https://github.com/golang/go/issues/32437#issuecomment-502975437
> This then leads to two distinct styles for error handling, where before there was only one. If this proposal is all we get to help with error handling in Go, then, I think it would be better to do nothing and keep handling error handling with if
like we always have, because at least, this is consistent and had the benefit of “there is only one way to do it”.
@beoran Agreed. This is why I suggested that we unify the vast majority of error cases under the try
keyword (try
and try
/else
). Even though the try
/else
syntax doesn’t give us any significant reduction in code length versus the existing if err != nil
style, it gives us consistency with the try
(no else
) case. Those two cases (try and try-else) are likely to cover the vast majority of error handling cases. I put that in opposition to the builtin no-else version of try
that only applies in cases where the programmer isn’t actually doing anything to handle the error besides returning (which, as others have mentioned in this thread, isn’t necessarily something we really want to encourage in the first place).
Consistency is important to readability.
append
is the definitive way to add elements to a slice. make
is the definitive way to construct a new channel or map or slice (with the exception of literals, which I’m not thrilled about). But try()
(as a builtin, and without else
) would be sprinkled throughout codebases, depending on how the programmer needs to handle a given error, in a way that’s probably a bit chaotic and confusing to the reader. It doesn’t seem to be in the spirit of the other builtins (namely, handling a case that’s either quite difficult or outright impossible to do otherwise). If this is the version of try
that succeeds, consistency and readability will compel me not to use it, just as I try to avoid map/slice literals (and avoid new
like the plague).
If the idea is to change how errors are handled, it seems wise to try to unify the approach across as many cases as possible, rather than adding something that, at best, will be “take it or leave it.” I fear the latter will actually add noise rather than reducing it.
@brynbellomy wrote:
Even though the try/else syntax doesn’t give us any significant reduction in code length versus the existing if err != nil style, it gives us consistency with the try (no else) case.
The unique goal of the try
built-in function is to reduce boilerplate, so it’s hard to see why we should adopt the try/else syntax you propose when you acknowledge that it “doesn’t give us any significant reduction in code length”.
You also mention that the syntax you propose makes the try case consistent with the try/else case. But it also creates an inconsistent way to branch, when we already have if/else. You gain a bit of consistency on a specific use case but lose a lot inconsistency on the rest.
I feel the need to express my opinions for what they are worth. Though not all of this is academic and technical in nature, I think it needs to be said.
I believe this change is one of these cases where engineering is being done for engineering sake and “progress” is being used for the justification. Error handling in Go is not broken and this proposal violates a lot of the design philosophy I love about Go.
Make things easy to understand, not easy to do
This proposal is choosing optimizing for laziness over correctness. The focus is on making error handling easier and in return a huge amount of readability is being lost. The occasional tedious nature of error handling is acceptable because of the readability and debuggability gains.
Avoid naming return arguments
There are a few edge cases with defer
statements where naming the return argument is valid. Outside of these, it should be avoided. This proposal promotes the use of naming return arguments. This is not going to help make Go code more readable.
Encapsulation should create a new semantics where one is absolutely precise
There is no precision in this new syntax. Hiding the error variable and the return does not help to make things easier to understand. In fact, the syntax feels very foreign from anything we do in Go today. If someone wrote a similar function, I believe the community would agree the abstraction is hiding the cost and not worth the simplicity it’s trying to provide.
Who are we trying to help?
I am concerned this change is being put in place in an attempt to entice enterprise developers away from their current languages and into Go. Implementing language changes, just to grow numbers, sets a bad precedent. I think it’s fair to ask this question and get an answer to the business problem that is attempting to be solved and the expected gain that is trying to be achieved?
I have seen this before several times now. It seems quite clear, with all the recent activity from the language team, this proposal is basically set in stone. There is more defending of the implementation then actual debate on the implementation itself. All of this started 13 days ago. We will see the impact this change has on the language, community and future of Go.
Error handling in Go is not broken and this proposal violates a lot of the design philosophy I love about Go.
Bill expresses my thoughts perfectly.
I can’t stop try
being introduced, but if it is, I won’t be using it myself; I won’t teach it, and I won’t accept it in PRs I review. It will simply be added to the list of other ‘things in Go I never use’ (see Mat Ryer’s amusing talk on YouTube for more of these).
@ardan-bkennedy, thanks for your comments.
You asked about the “business problem that is attempting to be solved”. I don’t believe we are targeting the problems of any particular business except maybe “Go programming”. But more generally we articulated the problem we are trying to solve last August in the Gophercon design draft discussion kickoff (see the Problem Overview especially the Goals section). The fact that this conversation has been going on since last August also flatly contradicts your claim that “All of this started 13 days ago.”
You are not the only person to have suggested that this is not a problem or not a problem worth solving. See https://swtch.com/try.html#nonissue for other such comments. We have noted those and do want to make sure we are solving an actual problem. Part of the way to find out is to evaluate the proposal on real code bases. Tools like Robert’s tryhard help us do that. I asked earlier for people to let us know what they find in their own code bases. That information will be critically important to evaluating whether the change is worthwhile or not. You have one guess and I have a different one, and that’s fine. The answer is to substitute data for those guesses.
We will do what is needed to make sure we are solving an actual problem. We’re not going to go through the effort of adding a language feature that will make Go programming worse overall.
Again, the path forward is experimental data, not gut reactions. Unfortunately, data takes more effort to collect. At this point, I would encourage people who want to help to go out and collect data.
@rsc There will be no shortage of locations where this convenience can be placed. What metric is being sought that will prove the substance of the mechanism aside from that? Is there a list of classified error handling cases? How will value be derived from the data when much of the public process is driven by sentiment?
@daved, re:
There will be no shortage of locations where this convenience can be placed. What metric is being sought that will prove the substance of the mechanism aside from that? Is there a list of classified error handling cases? How will value be derived from the data when much of the public process is driven by sentiment?
The decision is based on how well this works in real programs. If people show us that try is ineffective in the bulk of their code, that’s important data. The process is driven by that kind of data. It is not driven by sentiment.
@daved
How will value be derived from the data when much of the public process is driven by sentiment?
Perhaps others may have an experience like mine reported here. I expected to flip through a few instances of try
inserted by tryhard
, find they looked more or less like what already existed in this thread, and move on. Instead, I was surprised to find a case in which try
led to clearly better code, in a way that had not been discussed before.
So there’s at least hope. :)
@ardan-bkennedy, sorry for the second followup but regarding:
I am concerned this change is being put in place in an attempt to entice enterprise developers away from their current languages and into Go. Implementing language changes, just to grow numbers, sets a bad precedent.
There are two serious problems with this line that I can’t walk past.
First, I reject the implicit claim that there are classes of developers – in this case “enterprise developers” – that are somehow not worthy of using Go or having their problems considered. In the specific case of “enterprise”, we are seeing plenty of examples of both small and large companies using Go very effectively.
Second, from the start of the Go project, we – Robert, Rob, Ken, Ian, and I – have evaluated language changes and features based on our collective experience building many systems. We ask “would this work well in the programs we write?” That has been a successful recipe with broad applicability and is the one we intend to keep using, again augmented by the data I asked for in the previous comment and experience reports more generally. We would not suggest or support a language change that we can’t see ourselves using in our own programs or that we don’t think fits well into Go. And we would certainly not suggest or support a bad change just to have more Go programmers. We use Go too after all.
@ardan-bkennedy Thanks for your thoughts. With all due respect, I believe you have misrepresented the intent of this proposal and made several unsubstantiated claims.
Regarding some of the points @rsc has not addressed earlier:
We have never said error handling is broken. The design is based on the observation (by the Go community!) that current handling is fine, but verbose in many cases - this is undisputed. This is a major premise of the proposal.
Making things easier to do can also make them easier to understand - these two don’t mutually exclude each other, or even imply one another. I urge you to look at this code for an example. Using try
removes a significant amount of boilerplate, and that boilerplate adds virtually nothing to the understandability of the code. Factoring out of repetitive code is a standard and widely accepted coding practice for improving code quality.
Regarding “this proposal violates a lot of the design philosophy”: What is important is that we don’t get dogmatic about “design philosophy” - that is often the downfall of good ideas (besides, I think we know a thing or two about Go’s design philosophy). There is a lot of “religious fervor” (for lack of a better term) around named vs unnamed result parameters. Mantras such as “you shall not use named result parameters ever” out of context are meaningless. They may serve as general guidelines, but not absolute truths. Named result parameters are not inherently “bad”. Well-named result parameters can add to the documentation of an API in meaningful ways. In short, let’s not use slogans to make language design decisions.
It is a point of this proposal to not introduce new syntax. It just proposes a new function. We can’t write that function in the language, so a built-in is the natural place for it in Go. Not only is it a simple function, it is also defined very precisely. We choose this minimal approach over more comprehensive solutions exactly because it does one thing very well and leaves almost nothing to arbitrary design decisions. We are also not wildly off the beaten track since other languages (e.g. Rust) have very similar constructs. Suggesting that the “the community would agree the abstraction is hiding the cost and not worth the simplicity it’s trying to provide” is putting words into other people’s mouth. While we can clearly hear the vocal opponents of this proposal, there is significant percentage (an estimated 40%) of people who expressed approval of going forward with the experiment. Let’s not disenfranchise them with hyperbole.
Thanks.
You are not the only person to have suggested that this is not a problem or not a problem worth solving. See https://swtch.com/try.html#nonissue for other such comments. We have noted those and do want to make sure we are solving an actual problem.
@rsc I also think there is no problem with current error code. So, please, count me in.
Tools like Robert’s tryhard help us do that. I asked earlier for people to let us know what they find in their own code bases. That information will be critically important to evaluating whether the change is worthwhile or not. You have one guess and I have a different one, and that’s fine. The answer is to substitute data for those guesses.
I looked at https://go-review.googlesource.com/c/go/+/182717/1/src/cmd/link/internal/ld/macho_combine_dwarf.go and I like old code better. It is surprising to me that try function call might interrupt current execution. That is not how current Go works.
I suspect, you will find opinions will vary. I think this is very subjective.
And, I suspect, majority of users are not participating in this debate. They don’t even know that this change is coming. I am pretty involved with Go myself, but I don’t participate in this change, because I have no free time.
I think we would need to re-educate all existing Go users to think differently now.
We would also need to decide what to do with some users / companies who will refuse to use try in their code. There will be some for sure.
Maybe we would have to change gofmt to rewrite current code automatically. To force such “rogue” users to use new try function. Is it possible to make gofmt do that?
How would we deal with compile errors when people use go1.13 and before to build code with try?
I probably missed many other problems we would have to overcome to implement this change. Is it worth the trouble? I don’t believe so.
Alex
@alexbrainman Thanks for your feedback.
A large number of comments on this thread are of the form “this doesn’t look like Go”, or “Go doesn’t work like that”, or “I’m not expecting this to happen here”. That is all correct, existing Go doesn’t work like that.
This is perhaps the first suggested language change that affects the feel of the language in more substantial ways. We are aware of that, which is why we kept it so minimal. (I have a hard time imagining the uproar a concrete generics proposal might cause - talking about a language change).
But going back to your point: Programmers get used to how a programming language works and feels. If I’ve learned anything over the course of some 35 years of programming is that one gets used to almost any language, and it happens very quickly. After having learned original Pascal as my first high-level language, it was inconceivable that a programming language would not capitalize all its keywords. But it only took a week or so to get used to the “sea of words” that was C where “one couldn’t see the structure of the code because it’s all lowercase”. After those initial days with C, Pascal code looked awfully loud, and all the actual code seemed buried in a mess of shouting keywords. Fast forward to Go, when we introduced capitalization to mark exported identifiers, it was a shocking change from the prior, if I remember correctly, keyword-based approach (this was before Go was public). Now we think it’s one of the better design decisions (with the concrete idea actually coming from outside the Go Team). Or, consider the following thought experiment: Imagine Go had no defer
statement and now somebody makes a strong case for defer
. defer
doesn’t have semantics like anything else in the language, the new language doesn’t feel like that pre-defer
Go anymore. Yet, after having lived with it for a decade it seems totally “Go-like”.
The point is, the initial reaction towards a language change is almost meaningless without actually trying the mechanism in real code and gathering concrete feedback. Of course, the existing error handling code is fine and looks clearer than the replacement using try
- we’ve been trained to think those if
statements away for a decade now. And of course try
code looks strange and has “weird” semantics, we have never used it before, and we don’t immediately recognize it as a part of the language.
Which is why we are asking people to actually engage with the change by experimenting with it in your own code; i.e., actually writing it, or have tryhard
run over existing code, and consider the result. I’d recommend to let it sit for a while, perhaps a week or so. Look at it again, and report back.
Finally, I agree with your assessment that a majority of people don’t know about this proposal, or have not engaged with it. It is quite clear that this discussion is dominated by perhaps a dozen or so people. But it’s still early, this proposal has only been out for two weeks, and no decision has been made. There is plenty of time for more and different people to engage with this.
…
@griesemer
This is perhaps the first suggested language change that affects the feel of the language in more substantial ways. We are aware of that, which is why we kept it so minimal. (I have a hard time imagining the uproar a concrete generics proposal might cause - talking about a language change).
I would rather you spend time on generics, rather then try. Maybe there is a benefit in having generics in Go.
But going back to your point: Programmers get used to how a programming language works and feels. …
I agree with all your points. But we are talking about replacing particular form of if statement with try function call. This is in the language that prides itself on simplicity and orthogonality. I can get used to everything, but what is the point? To save couple lines of code?
Or, consider the following thought experiment: Imagine Go had no
defer
statement and now somebody makes a strong case fordefer
.defer
doesn’t have semantics like anything else in the language, the new language doesn’t feel like that pre-defer
Go anymore. Yet, after having lived with it for a decade it seems totally “Go-like”.
After many years I still get tricked by defer
body and closed over variables. But defer
pays its price in spades when it comes down to resource management. I cannot imagine Go without defer
. But I am not prepared to pay similar price for try
, because I see no benefits here.
Which is why we are asking people to actually engage with the change by experimenting with it in your own code; i.e., actually writing it, or have
tryhard
run over existing code, and consider the result. I’d recommend to let it sit for a while, perhaps a week or so. Look at it again, and report back.
I tried changing small project of mine (about 1200 lines of code). And it looks similar to your change at https://go-review.googlesource.com/c/go/+/182717/1/src/cmd/link/internal/ld/macho_combine_dwarf.go I don’t see my opinion change about this after a week. My mind is always occupied with something, and I will forget.
… But it’s still early, this proposal has only been out for two weeks, …
And I can see there are 504 messages about this proposal just on this thread already. If I would be interested in pushing this change along, it will take me days if not weeks just to read and comprehend this all. I don’t envy your job.
Thank you for taking time to reply to my message. Sorry, if I won’t reply to this thread - it just too large for me to monitor, if message is addressed to me or not.
Alex
I’m totally on board with this proposal and can see its benefits across a number of examples.
My only concern with the proposal is the naming of try
, I feel that its connotations with other languages, may skew deveopers perceptions of what its purpose is when coming from other languages. Java comes to find here.
For me, i would prefer the builtin to be called pass
. I feel this gives a better representation of what is happening. After-all you are not handling the error - rather passing it back to be handled by the caller. try
gives the impression that the error has been handled.
I don’t like the try
name. It implies an attempt at doing something with a high risk of failure (I may have a cultural bias against try as I’m not a native english speaker), while instead try
would be used in case we expect rare failures (motivation for wanting to reduce verbosity of error handling) and are optimistic. In addition try
in this proposal does in fact catches an error to return it early. I like the pass
suggestion of @HiImJC.
…
Secondly, I don’t mind that a built in that can cause the function to return, but, to bikeshed a bit, the name ‘try’ is too short to suggest that it can cause a return. So a longer name, like attempt
seems better to me.
…
9) Finally, some people don’t like the name try
(@beoran, @HiImJC, @dolmen) or would prefer a symbol such as ?
(@twisted1919, @leaxoy, others).
…
9) May I suggest that we don’t bike-shed the name at this point. Once all the other concerns are settled is a better time to fine-tune the name.
…
Also, try
is not “trying” anything. It is a “protective relay”. If the base semantics of the proposal is off, I’m not surprised the resulting code is also problematic.
func (f *http2Framer) endWrite() error {
...
relay n := f.w.Write(f.wbuf)
return checkShortWrite(n, len(f.wbuf))
}
Is it possible to make it work without brackets?
I.e. something like:
a := try func(some)
@Cyberax - As already mentioned above, it is very essential that you read the design doc carefully before posting. Since this is a high-traffic issue, with a lot of people subscribed.
The doc discusses operators vs functions in detail.
…
I’ve read the proposal’s justification for making it a builtin rather than a keyword, and it boils down to not having to adjust the parser. But isn’t that a relatively small amount of pain for compiler and tooling writers, whereas having the extra parens and the this-looks-like-a-function-but-isn’t readability issues will be something all Go coders and code-readers have to endure. In my opinion the argument (excuse? :-) that “but panic()
does control flow” doesn’t cut it, because panic and recover are by their very nature, exceptional, whereas try()
will be normal error handling and control flow.
I’d definitely appreciate it even if this went in as is, but my strong preference would be for normal control flow to be clear, i.e., done via a keyword.
@benhoyt Thanks for your positive feedback.
If the main argument against this proposal is the fact that try
is a built-in, we’re in a great spot. Using a built-in is simply a pragmatic solution for the backward-compatibility problem (it happens to cause no extra work for the parser, and tools etc. - but that’s just a nice side benefit, not the primary reason). There are also some benefits to having to write parentheses, this is discussed in detail in the design doc (section on Properties of the proposed design).
All that said, if using a built-in is the showstopper, we should consider the try
keyword. It will not be backward-compatible though with existing code as the keyword may conflict with existing identifiers.
(To be complete, there’s also the option of an operator such as ?
, which would be backward-compatible. It doesn’t strike me as the best choice for a language such as Go, though. But again, if that’s all it takes to make try
palatable, we should perhaps consider it.)
I personally liked the earlier check
proposal more than this, based on purely visual aspects; check
had the same power as this try()
but bar(check foo())
is more readable to me than bar(try(foo()))
(I just needed a second to count the parens!).
To me making “try” a built-in function and enabling chains has 2 problems: - It is inconsistent with the rest of the control flow in go (e.g, for/if/return/etc keywords). - It makes code less readable.
I would prefer making it a statement without parenthesis. The examples in the proposal would require multiple lines but would become more readable (i.e, individual “try” instances would be harder to miss). Yes, it would break external parsers but I prefer to preserve consistency.
The ternary operator is another place were go does not have something and requires more keystrokes but at the same time improves readability/maintainability. Adding “try” in this more restricted form will better balance expressiveness vs readability, IMO.
FWIW, panic
affects control flow and has parens, but go
and defer
also affect flow and don’t. I tend to think that try
is more similar to defer
in that it’s an unusual flow operation and making it harder to do try (try os.Open(file)).Read(buf)
is good because we want to discourage one-liners anyway, but whatever. Either is fine.
I appreciate the commitment to backwards compatibility that motivates you to make try
a builtin, rather than a keyword, but after wrestling with the utter weirdness of having a frequently-used function that can change control flow (panic
and recover
are extremely rare), I got to wondering: has anyone done any large-scale analysis of the frequency of try
as an identifier in open source codebases? I was curious and skeptical, so I did a preliminary search across the following:
Across the 11,108,770 significant lines of Go living in these repositories, there were only 63 instances of try
being used as an identifier. Of course, I realize that these codebases (while large, widely used, and important in their own right) represent only a fraction of the Go code out there, and additionally, that we have no way to directly analyze private codebases, but it’s certainly an interesting result.
Moreover, because try
, like any keyword, is lowercase, you’ll never find it in a package’s public API. Keyword additions will only affect package internals.
This is all preface to a few ideas I wanted to throw into the mix which would benefit from try
as a keyword.
@brynbellomy Thanks for the keyword analysis - that’s very helpful information. It does seem that try
as a keyword might be ok. (You say that APIs are not affected - that’s true, but try
might still show up as parameter name or the like - so documentation may have to change. But I agree that would not affect clients of those packages.)
…
1) In general, using a built-in for the try
functionality is felt to be a bad choice: Given that it affects control flow it should be at least a keyword (@carloslenz “prefers making it a statement without parenthesis”); try
as an expression seems not a good idea, it harms readability (@ChrisHines, @jimmyfrasche), they are “returns without a return
”. @brynbellomy did an actual analysis of try
used as identifiers; there appear to be very few percentage-wise, so it might be possible to go the keyword route w/o affecting too much code.
It is counter intuitive as a function. Because it is not possible in Go to have function with this order of arguments func(... interface{}, error)
.
Typed first then variable number of anything pattern is everywhere in Go modules.
@mishak87 We address this in the detailed proposal. Note that we have other built-ins (try
, make
, unsafe.Offsetof
, etc.) that are “irregular” - that’s what built-ins are for.
@rsc, As you may have seen, while I suggested the try block syntax, I later reverted to the “no” side for this feature – partly because I feel uncomfortable hiding one or more conditional error returns in a statement or function application. But let me clarify one point. In the try block proposal I explicitly allowed statements that don’t need try
. So your last try block example would be:
try (
headRef := r.Head()
parentObjOne := headRef.Peel(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()
tree := r.LookupTree(treeOid)
)
This simply says that any errors returned within the try block are returned to the caller. If the control makes past the try block, there were no errors in the block.
You said > I think you should notice the core logic of what the program is doing first, and error handling later.
This exactly the reason I thought of a try block! What is factored out is not just the keyword but the error handling. I don’t want to have to think about N different places that may generate errors (except when I am explicitly trying to handle specific errors).
Some more points that may be worth mentioning:
try(try(foo(try(bar)).fum())
are allowed. Such use may be frowned upon but their semantics need to be specified. In the try block case the compiler has to work harder to detect such uses and squeeze out all error handling to the try block level.return-on-error
instead of try
. This is easier to swallow at a block level!FWIW, I still don’t think this is worth doing.
In the try block proposal I explicitly allowed statements that don’t need try
The main advantage in Go’s error handling that I see over the try-catch system of languages like Java and Python is that it’s always clear which function calls may result in an error and which cannot. The beauty of try
as documented in the original proposal is that it can cut down on simple error handling boilerplate while still maintaining this important feature.
To borrow from @Goodwine ’s examples, despite its ugliness, from an error handling perspective even this:
parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())
… is better than what you often see in try-catch languages
parentCommitOne := r.Head().Peel(git.ObjectCommit).AsCommit()
parentCommitTwo := remoteBranch.Reference.Peel(git.ObjectCommit).AsCommit()
… because you can still tell what parts of the code may divert control flow due to an error and which cannot.
I know that @bakul isn’t advocating for this block syntax proposal anyway, but I think it brings up an interesting point about Go’s error handling in comparison to others. I think it’s important that any error handling proposal Go adopts should not obfuscate what parts of the code can and cannot error out.
@bakul,
But let me clarify one point. In the try block proposal I explicitly allowed statements that don’t need
try
.
Doing this would fall short of the second goal: “Both error checks and error handling must remain explicit, meaning visible in the program text. We do not want to repeat the pitfalls of exception handling.”
The main pitfall of traditional exception handling is not knowing where the checks are. Consider:
try {
s = canThrowErrors()
t = cannotThrowErrors()
u = canThrowErrors() // a second call
} catch {
// how many ways can you get here?
}
If the functions were not so helpfully named, it can be very difficult to tell which functions might fail and which are guaranteed to succeed, which means you can’t easily reason about which fragments of code can be interrupted by an exception and which cannot.
Compare this with Swift’s approach, where they adopt some of the traditional exception-handling syntax but are actually doing error handling, with an explicit marker on each checked function and no way to unwind beyond the current stack frame:
do {
let s = try canThrowErrors()
let t = cannotThrowErrors()
let u = try canThrowErrors() // a second call
} catch {
handle error from try above
}
Whether it’s Rust or Swift or this proposal, the key, critical improvement over exception handling is explicitly marking in the text - even with a very lightweight marker - each place where a check is.
For more about the problem of implicit checks, see the Problem section of the problem overview from last August, in particular the links to the two Raymond Chen articles.
Edit: see also @velovix’s comment three up, which came in while I was working on this one.
I agree this is the best way forward: fixing the most common issue with a simple design.
I don’t want to bikeshed (feel free to postpone this conversation), but Rust went there and eventually settled with the ?
postfix operator rather than a builtin function, for increased readability.
The gophercon proposal cites ?
in the considered ideas and gives three reason why it was discarded: the first (“control flow transfers are as a general rule accompanied by keywords”) and the third (“handlers are more naturally defined with a keyword, so checks should too”) do not apply anymore. The second is stylistic: it says that, even if the postfix operator works better for chaining, it can still read worse in some cases like:
check io.Copy(w, check newReader(foo))
rather than:
io.Copy(w, newReader(foo)?)?
but now we would have:
try(io.Copy(w, try(newReader(foo))))
which I think it’s clearly the worse of the three, as it’s not even obvious anymore which is the main function being called.
So the gist of my comment is that all three reasons cited in the gophercon proposal for not using ?
do not apply to this try
proposal; ?
is concise, very readable, it does not obscure the statement structure (with its internal function call hierarchy), and it is chainable. It removes even more clutter from the view, while not obscuring the control flow more than the proposed try()
already does.
I think the ?
would be a better fit than try
, and always having to chase the defer
for error would also be tricky.
How about use only ?
to unwrap result just like rust
On the subject of try
as a predefined identifier rather than an operator,
I found myself trending towards a preference for the latter after repeatedly getting the brackets wrong when writing out:
try(try(os.Create(filename)).Write(data))
Under “Why can’t we use ? like Rust”, the FAQ says:
So far we have avoided cryptic abbreviations or symbols in the language, including unusual operators such as ?, which have ambiguous or non-obvious meanings.
I’m not entirely sure that’s true. The .()
operator is unusual until you know Go, as are the channel operators. If we added a ?
operator, I believe that it would shortly become ubiquitous enough that it wouldn’t be a significant barrier.
The Rust ?
operator is added after the closing bracket of a function call though, and that means that it’s easy to miss when the argument list is long.
How about adding ?()
as an call operator:
So instead of:
x := try(foo(a, b))
you’d do:
x := foo?(a, b)
The semantics of ?()
would be very similar to those of the proposed try
built-in. It would act like a function call except that the function or method being called must return an error as its last argument. As with try
, if the error is non-nil, the ?()
statement will return it.
@rogpeppe I think that kind of subtle operator is only reasonable for calls that should never return error, and so panic if they do. Or calls where you always ignore the error. But both seem to be rare. If you’re open to a new operator, see #32500.
I suggested that f(try g())
should panic in https://github.com/golang/go/issues/32437#issuecomment-501074836, along with a 1-line handling stmt:
on err, return ...
@rogpeppe In your comment about getting the parentheses right with nested try
’s: Do you have any opinions on the precedence of try
? See also the detailed design doc on this subject.
@griesemer I’m indeed not that keen on try
as a unary prefix operator for the reasons pointed out there. It has occurred to me that an alternative approach would be to allow try
as a pseudo-method on a function return tuple:
f := os.Open(path).try()
That solves the precedence issue, I think, but it’s not really very Go-like.
@rogpeppe
Very interesting! . You may really be on to something here.
And how about we extend that idea like so?
for _,fp := range filepaths {
f := os.Open(path).try(func(err error)bool{
fmt.Printf( "Cannot open file %s\n", fp );
continue;
});
}
BTW, I might prefer a different name vs try()
such as maybe guard()
but I shouldn’t bikeshed the name prior to the architecture being discussed by others.
vs :
for _,fp := range filepaths {
if f,err := os.Open(path);err!=nil{
fmt.Printf( "Cannot open file %s\n", fp )
}
}
?
Syntax
This discussion has identified six different syntaxes to write the same semantics from the proposal:
f := try(os.Open(file))
, from the proposal (builtin function)f := try os.Open(file)
, using a keyword (prefix keyword)f := os.Open(file)?
, like in Rust (call-suffix operator)f := os.Open?(file)
, suggested by @rogpeppe (call-infix operator)try f := os.Open(file)
, suggested by @thepudds (try statement)try ( f := os.Open(file); f.Close() )
, suggested by @bakul (try block)(Apologies if I got the origin stories wrong!)
All of these have pros and cons, and the nice thing is that because they all have the same semantics, it is not too important to choose between the various syntaxes in order to experiment further.
I found this example by @brynbellomy thought-provoking:
headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := try(parentObjOne.AsCommit())
parentCommitTwo := try(parentObjTwo.AsCommit())
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))
// vs
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
try parentCommitOne := parentObjOne.AsCommit()
try parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)
// vs
try (
headRef := r.Head()
parentObjOne := headRef.Peel(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()
tree := r.LookupTree(treeOid)
)
There is not much difference between these specific examples, of course. And if the try is there in all the lines, why not line them up or factor them out? Isn’t that cleaner? I wondered about this too.
But as @ianlancetaylor observed, “the try buries the lede. Code becomes a series of try statements, which obscures what the code is actually doing.”
I think that’s a critical point: lining up the try that way, or factoring it out as in the block, implies a false parallelism. It implies that what’s important about these statements is that they all try. That’s typically not the most important thing about the code and not what we should be focused on when reading it.
Suppose for sake of argument that AsCommit never fails and consequently does not return an error. Now we have:
headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))
// vs
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)
// vs
try (
headRef := r.Head()
parentObjOne := headRef.Peel(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try (
treeOid := index.WriteTree()
tree := r.LookupTree(treeOid)
)
What you see at first glance is that the middle two lines are clearly different from the others. Why? It turns out because of error handling. Is that the most important detail about this code, the thing you should notice at first glance? My answer is no. I think you should notice the core logic of what the program is doing first, and error handling later. In this example, the try statement and try block hinder that view of the core logic. For me, this suggests they are not the right syntax for these semantics.
That leaves the first four syntaxes, which are even more similar to each other:
headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))
// vs
headRef := try r.Head()
parentObjOne := try headRef.Peel(git.ObjectCommit)
parentObjTwo := try remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try index.WriteTree()
tree := try r.LookupTree(treeOid)
// vs
headRef := r.Head()?
parentObjOne := headRef.Peel(git.ObjectCommit)?
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)?
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()?
tree := r.LookupTree(treeOid)?
// vs
headRef := r.Head?()
parentObjOne := headRef.Peel?(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel?(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree?()
tree := r.LookupTree?(treeOid)
It’s hard to get too worked up about choosing one over the others. They all have their good and bad points. The most important advantages of the builtin form are that:
(1) the exact operand is very clear, especially compared to prefix-operator try x.y().z()
.
(2) tools that don’t need to know about try can treat it as a plain function call, so for example goimports will work fine without any adjustments, and
(3) there is some room for future expansion and adjustment if needed.
It is entirely possible that after seeing real code using these constructs, we will develop a better sense for whether the advantages of one of the other three syntaxes outweigh these advantages of the function call syntax. Only experiments and experience can tell us this.
In response to https://github.com/golang/go/issues/32437#issuecomment-502837008 (@rsc’s comment about try
as a statement)
You raise a good point. I’m sorry that I had somehow missed that comment before making this one: https://github.com/golang/go/issues/32437#issuecomment-502871889
Your examples with try
as an expression look much better than the ones with try
as a statement. The fact that the statement leads with try
does in fact make it much harder to read. However, I am still worried that people will nest try calls together to make bad code, as try
as an expression really encourages this behavior in my eyes.
I think I would appreciate this proposal a bit more if golint
prohibited nested try
calls. I think that prohibiting all try
calls inside of other expressions is a bit too strict, having try
as an expression does have its merits.
Borrowing your example, even just nesting 2 try calls together looks quite hideous, and I can see Go programmers doing it, especially if they work without code reviewers.
parentCommitOne := try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit()
parentCommitTwo := try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit()
tree := try(r.LookupTree(try(index.WriteTree())))
The original example actually looked quite nice, but this one shows that nesting the try expressions (even only 2-deep) really does hurt the readability of the code drastically. Denying nested try
calls would also help with the “debuggability” issue, since it’s much easier to expand a try
into an if
if it’s on the outside of an expression.
Again, I’d almost like to say that a try
inside a sub-expression should be flagged by golint
, but I think that might be a little too strict. It would also flag code like this, which in my eyes is fine:
x := 5 + try(strconv.Atoi(input))
This way, we get both the benefits of having try
as an expression, but we aren’t promoting adding too much complexity to the horizontal axis.
Perhaps another solution would be that golint
should only allow a maximum of 1 try
per statement, but it’s late, I’m getting tired, and I need to think about it more rationally. Either way, I have been quite negative toward this proposal at some points, but I think I can actually turn to really liking it as long as there are some golint
standards related to it.
@deanveloper wrote:
I think I would appreciate this proposal a bit more if golint prohibited nested try calls.
I agree that deeply nested try
could be hard to read. But this is also true for standard function calls, not just the try
built-in function. Thus I don’t see why golint
should forbid this.
@griesemer Thanks for the wonderful proposal and tryhard seems to be a more useful that I expect. I will also want to appreciate.
@rsc thanks for the well-articulated response and tool.
Have been following this thread for a while and the following comments by @beoran give me chills
Hiding the error variable and the return does not help to make things easier to understand
Have have had managed several bad written code
before and I can testify it’s the worst nightmare for every developer.
The fact the documentation says to use A
likes does not mean it would be followed, the fact remains if it’s possible to use AA
, AB
then there is no limit to how it can be used.
To my surprise, people already think the code below is cool
… I think it's an abomination
with all due respect apologies to anyone offended.
parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())
Wait until you check AsCommit
and you see
func AsCommit() error(){
return try(try(try(tail()).find()).auth())
}
The madness goes on and honestly I don’t want to believe this is the definition of @robpike simplicity is complicated
(Humor)
Based on @rsc example
// Example 1
headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))
// Example 2
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)
// Example 3
try (
headRef := r.Head()
parentObjOne := headRef.Peel(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try (
treeOid := index.WriteTree()
tree := r.LookupTree(treeOid)
)
Am in favour of Example 2
with a little else
, Please note that this might not be the best approach however
abomination
the others can give birth totry
doesn’t behave like a normal function. to give it function-like syntax is little of. go
uses if
and if I can just change it to try tree := r.LookupTree(treeOid) else {
it feels more naturaltry
& catch
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid) else {
// Heal the world
// I may return with return keyword
// I may not return but set some values to 0
// I may remember I need to log only this
// I may send a mail to let the cute monkeys know the server is on fire
}
Once again I want to apologise for being a little selfish.
@olekukonko, re https://github.com/golang/go/issues/32437#issuecomment-503508478:
To my surprise, people already think the code below is cool
… I thinkit's an abomination
with all due respect apologies to anyone offended.parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit()) parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())
Grepping https://swtch.com/try.html, that expression has occurred three times in this thread. @goodwine brought it up as bad code, I agreed, and @velovix said “despite its ugliness … is better than what you often see in try-catch languages … because you can still tell what parts of the code may divert control flow due to an error and which cannot.”
No one said it was “cool” or something to put forth as great code. Again, it’s always possible to write bad code.
I would also just say re
Errors can be very very expensive, they need as much visibility as possible
Errors in Go are meant to not be expensive. They are everyday, ordinary occurrences and meant to be lightweight. (This is in contrast to some implementations of exceptions in particular. We once had a server that spent far too much of its CPU time preparing and discarding exception objects containing stack traces for failed “file open” calls in a loop checking a list of known locations for a given file.)
Instead of using a function, would just be more idiomatic using a special identifier?
We already have the blank identifier _
which ignores values.
We could have something like #
which can only be used in functions which have the last returned value of type error.
func foo() (error) {
f, # := os.Open()
defer f.Close()
_, # = f.WriteString("foo")
return nil
}
when a error is assigned to #
the function returns immediately with the error received. As for the other variables their values would be:
…
This also closes the gates for having exceptions using try/catch
forever.
This also closes the gates for having exceptions using try/catch forever.
I am more than okay with this.
…
Also, the naming of try
will automatically imply that eventually catch
will show up in the syntax, and we’ll be back to being Java.
Being chaotic here, I’ll throw the idea of adding a second built-in function called catch
which will receive a function that takes an error and returns an overwritten error, then if a subsequent catch
is called it would overwrite the handler. for example:
func catch(handler func(err error) error) {
// .. impl ..
}
Now, this builtin function will also be a macro-like function that would handle the next error to be returned by try
like this:
func wrapf(format string, ...values interface{}) func(err error) error {
// user defined
return func(err error) error {
return fmt.Errorf(format + ": %v", ...append(values, err))
}
}
func sample() {
catch(wrapf("something failed in foo"))
try(foo()) // "something failed in foo: <error>"
x := try(foo2()) // "something failed in foo: <error>"
// Subsequent calls for catch overwrite the handler
catch(wrapf("something failed in bar with x=%v", x))
try(bar(x)) // "something failed in bar with x=-1: <error>"
}
This is nice because I can wrap errors without defer
which can be error prone unless we use named return values or wrap with another func, it is also nice because defer
would add the same error handler for all errors even if I want to handle 2 of them differently. You can also use it as you see fit, for example:
func foo(a, b string) (int64, error) {
return try(strconv.Atoi(a)) + try(strconv.Atoi(b))
}
func withContext(a, b string) (int64, error) {
catch(func (err error) error {
return fmt.Errorf("can't parse a: %s, b: %s, err: %v", a, b, err)
})
return try(strconv.Atoi(a)) + try(strconv.Atoi(b))
}
func moreExplicitContext(a, b string) (int64, error) {
catch(func (err error) error {
return fmt.Errorf("can't parse a: %s, err: %v", a, err)
})
x := try(strconv.Atoi(a))
catch(func (err error) error {
return fmt.Errorf("can't parse b: %s, err: %v", b, err)
})
y := try(strconv.Atoi(b))
return x + y
}
func withHelperWrapf(a, b string) (int64, error) {
catch(wrapf("can't parse a: %s", a))
x := try(strconv.Atoi(a))
catch(wrapf("can't parse b: %s", b))
y := try(strconv.Atoi(b))
return x + y
}
func before(a, b string) (int64, error) {
x, err := strconv.Atoi(a)
if err != nil {
return 0, fmt.Errorf("can't parse a: %s, err: %v", a, err)
}
y, err := strconv.Atoi(b)
if err != nil {
return 0, fmt.Errorf("can't parse b: %s, err: %v", b, err)
}
return x + y
}
And still on the chaotic mood (to help you empathize) If you don’t like catch
, you don’t have to use it.
Now… I don’t really mean it the last sentence, but it does feel like it’s not helpful for the discussion, very aggressive IMO.
Still, if we went this route I think that we may as well have try{}catch(error err){}
instead :stuck_out_tongue:
See also #27519 - the #id/catch error model
I think we can also add a catch function, which would be a nice pair, so:
func a() int {
x := randInt()
// let's assume that this is what recruiters should "fix" for us
// or this happens in 3rd-party package.
if x % 1337 != 0 {
panic("not l33t enough")
}
return x
}
func b() error {
// if a() panics, then x = 0, err = error{"not l33t enough"}
x, err := catch(a())
if err != nil {
return err
}
sendSomewhereElse(x)
return nil
}
// which could be simplified even further
func c() error {
x := try(catch(a()))
sendSomewhereElse(x)
return nil
}
in this example, catch()
would recover()
a panic and return ..., panicValue
.
of course, we have an obvious corner case in which we have a func, that also returns an error. in this case I think it would be convenient to just pass-thru error value.
so, basically, you can then use catch() to actually recover() panics and turn them into errors.
this looks quite funny for me, ‘cause Go doesn’t actually have exceptions, but in this case we have pretty neat try()-catch() pattern, that also shouldn’t blow up your entire codebase with something like Java (catch(Throwable)
in Main + throws LiterallyAnything
). you can easily process someone’s panics like those was usual errors. I’ve currently have about 6mln+ LoC in Go in my current project, and I think this would simplify things at least for me.
Using defer for handling the errors make a lot of sense, but it leads to needing to name the error and a new kind of if err != nil
boilerplate.
External handlers need to do this:
func handler(err *error) {
if *err != nil {
*err = handle(*err)
}
}
which gets used like
defer handler(&err)
External handlers need only be written once but there would need to be two versions of many error handling functions: the one meant to be defer’d and the one to be used in the regular fashion.
Internal handlers need to do this:
defer func() {
if err != nil {
err = handle(err)
}
}()
In both cases, the outer function’s error must be named to be accessed.
As I mentioned earlier in the thread, this can be abstracted into a single function:
func catch(err *error, handle func(error) error) {
if *err != nil && handle != nil {
*err = handle(*err)
}
}
That runs afoul of @griesemer’s concern over the ambiguity of nil
handler funcs and has its own defer
and func(err error) error
boilerplate, in addition to having to name err
in the outer function.
If try
ends up as a keyword, then it could make sense to have a catch
keyword, to be described below, as well.
Syntactically, it would be much like handle
:
catch err {
return handleThe(err)
}
Semantically, it would be sugar for the internal handler code above:
defer func() {
if err != nil {
err = handleThe(err)
}
}()
Since it’s somewhat magical, it could grab the outer function’s error, even if it was not named. (The err
after catch
is more like a parameter name for the catch
block).
catch
would have the same restriction as try
that it must be in a function that has a final error return, as they’re both sugar that relies on that.
That’s nowhere near as powerful as the original handle
proposal, but it would obviate the requirement to name an error in order to handle it and it would remove the new boilerplate discussed above for internal handlers while making it easy enough to not require separate versions of functions for external handlers.
Complicated error handling may require not using catch
the same as it may require not using try
.
Since these are both sugar, there’s no need to use catch
with try
. The catch
handlers are run whenever the function returns a non-nil
error, allowing, for example, sticking in some quick logging:
catch err {
log.Print(err)
return err
}
or just wrapping all returned errors:
catch err {
return fmt.Errorf("foo: %w", err)
}
Although I like the catch proposal by @jimmyfrasche, I would like to propose an alternative:
handler fmt.HandleErrorf("copy %s %s", src, dst)
would be equivalent to:
defer func(){
if(err != nil){
fmt.HandleErrorf(&err,"copy %s %s", src, dst)
}
}()
where err is the last named return value, with type error. However, handlers may also be used when return values are not named. The more general case would be allowed too:
handler func(err *error){ *err = fmt.Errorf("foo: %w", *err) }() `
The main problem I have with using named return values (which catch does not solve) is that err is superfluous. When deferring a call to a handler like fmt.HandleErrorf
, there is no reasonable first argument except a pointer to the error return value, why giving the user the option to make a mistake?
Compared with catch, the main difference is that handler makes a bit easier to call predefined handlers at the expense of making more verbose to define them in place. I am not sure this is ideal, but I think it is more in line with the original proposal.
@yiyus catch
, as I defined it doesn’t require err
to be named on the function containing the catch
.
In catch err {
, the err
is what the error is named within the catch
block. It’s like a function parameter name.
With that, there’s no need for something like fmt.HandleErrorf
because you can just use the regular fmt.Errorf
:
func f() error {
catch err {
return fmt.Errorf("foo: %w", err)
}
return errors.New("bar")
}
which returns an error that prints as foo: bar
.
@carlmjohnson I like @jimmyfrasche’s catch
idea regarding your point 2 - it’s just syntax sugar for a defer
which saves 2 lines and lets you avoid having to name the error return value (which in turn would also require you to name all the other ones if you hadn’t already). It doesn’t raise an orthogonality issue with defer
, because it is defer
.
please use try {} catch{} syntax, don’t build more wheels
@jimwei
Exception-based error handling might be a pre-existing wheel but it also has quite a few known problems. The problem statement in the original draft design does a great job of outlining these issues.
To add my own less well thought out commentary, I think it’s interesting that many very successful newer languages (namely Swift, Rust, and Go) have not adopted exceptions. This tells me that the broader software community is rethinking exceptions after the many years we’ve had to work with them.
please use try {} catch{} syntax, don’t build more wheels
i think it’s appropriate to build better wheels when the wheels that other people use are shaped like squares
I am really unhappy with a built-in function affecting control flow of the caller. This is very unintuitive and a first for Go. I appreciate the impossibility of adding new keywords in Go 1, but working around that issue with magic built-in functions just seems wrong to me. It’s worsened by the fact that built-ins can be shadowed, which drastically changes the way try(foo)
behaves. Shadowing of other built-ins doesn’t have results as unpredictable as control flow changing. It makes reading snippets of code without all of the context much harder.
I don’t like the way postfix ?
looks, but I think it still beats try()
. As such, I agree with @rasky .
Edit: Well, I managed to completely forget that panic exists and isn’t a keyword.
@dominikh The detailed proposal discusses this at length, but please note that panic
and recover
are two built-ins that affect control flow as well.
I retract my previous concerns about control flow and I no longer suggest using ?
. I apologize for the knee-jerk response (though I’d like to point out this wouldn’t have happened had the issue been filed after the full proposal was available).
I disagree with the necessity for simplified error handling, but I’m sure that is a losing battle. try
as laid out in the proposal seems to be the least bad way of doing it.
…
@lestrrat: Agreed that one will have to learn that try
can change control flow. We suspect that IDE’s could highlight that easily enough.
One last criticism. Not really a criticism to the proposal itself, but instead a criticism to a common response to the “function controlling flow” counterargument.
The response to “I don’t like that a function is controlling flow” is that “panic
also controls the flow of the program!“. However, there are a few reasons that it’s more okay for panic
to do this that don’t apply to try
.
panic
is friendly to beginner programmers because what it does is intuitive, it continues unwrapping the stack. One shouldn’t even have to look up how panic
works in order to understand what it does. Beginner programmers don’t even need to worry about recover
, since beginners aren’t typically building panic recovery mechanisms, especially since they are nearly always less favorable than simply avoiding the panic in the first place.
panic
is a name that is easy to see. It brings worry, and it needs to. If one sees panic
in a codebase, they should be immediately thinking of how to avoid the panic, even if it’s trivial.
Piggybacking off of the last point, panic
cannot be nested in a call, making it even easier to see.
It is okay for panic to control the flow of the program because it is extremely easy to spot, and it is intuitive as to what it does.
The try
function satisfies none of these points.
One cannot guess what try
does without looking up the documentation for it. Many languages use the keyword in different ways, making it hard to understand what it would mean in Go.
try
does not catch my eye, especially when it is a function. Especially when syntax highlighting will highlight it as a function. ESPECIALLY after developing in a language like Java, where try
is seen as unnecessary boilerplate (because of checked exceptions).
try
can be used in an argument to a function call, as per my example in my previous comment proc := try(os.FindProcess(try(strconv.Atoi(os.Args[1]))))
. This makes it even harder to spot.
My eyes ignore the try
functions, even when I am specifically looking for them. My eyes will see them, but immediately skip to the os.FindProcess
or strconv.Atoi
calls. try
is a conditional return. Control flow AND returns are both held up on pedestals in Go. All control flow within a function is indented, and all returns begin with return
. Mixing both of these concepts together into an easy-to-miss function call just feels a bit off.
This comment and my last are my only real criticisms to the idea though. I think I may be coming off as not liking this proposal, but I still think that it is an overall win for Go. This solution still feels more Go-like than the other solutions. If this were added I would be happy, however I think that it can still be improved, I’m just not sure how.
Hacker news has some point: try
doesn’t behave like a normal function (it can return) so it’s not good to give it function-like syntax. A return
or defer
syntax would be more appropriate:
func CopyFile(src, dst string) (err error) {
r := try os.Open(src)
defer r.Close()
w := try os.Create(dst)
defer func() {
w.Close()
if err != nil {
os.Remove(dst) // only if a “try” fails
}
}()
try io.Copy(w, r)
try w.Close()
return nil
}
@sheerun the common counterargument to this is that panic
is also a control-flow altering built-in function. I personally disagree with it, however it is correct.
panic(...)
is a relatively clear exception (pun not intended) to the rule that return
is the only way out of a function. I don’t think we should use its existence as justification to add a third.To echo @peterbourgon and @deanveloper, one of my favourite things about Go is that code flow is clear and panic() is not treated like a standard flow control mechanism in the way it is in Python.
Regarding the debate on panic, panic() almost always appears by itself on a line because it has no value. You can’t fmt.Println(panic("oops"))
. This increases its visibility tremendously and makes it far less comparable to try()
than people are making out.
If there is to be another flow control construct for functions, I would far prefer that it be a statement guaranteed to be the leftmost item on a line.
A builtin function that returns is a harder sell than a keyword that does the same.
I would like it more if it were a keyword like it is in Zig[1].
Built-in functions, whose type signature cannot be expressed using the language’s type system, and whose behavior confounds what a function normally is, just seems like an escape hatch that can be used repeatedly to avoid actual language evolution.
try
proposal is not consistent with these basic tenets, as it will promote shorthand at the cost of control-flow readability.try
built-in a statement instead of a function. Then it is more consistent with other control-flow statements like if
. Additionally removal of the nested parentheses marginally improves readability.defer
or similar. It already cannot be implemented in pure go (as pointed out by others) so it may as well use a more efficient implementation under the hood.I would like to repeat something @deanveloper and a few others have said, but with my own emphasis. In https://github.com/golang/go/issues/32437#issuecomment-498939499 @deanveloper said:
try
is a conditional return. Control flow AND returns are both held up on pedestals in Go. All control flow within a function is indented, and all returns begin withreturn
. Mixing both of these concepts together into an easy-to-miss function call just feels a bit off.
Furthermore, in this proposal try
is a function that returns values, so it may be used as part of a bigger expression.
Some have argued that panic
has already set the precedent for a built in function that changes control flow, but I think panic
is fundamentally different for two reasons:
Try on the other hand:
For these reasons I think try
feels more than a “bit off”, I think it fundamentally harms code readability.
Today, when we encounter some Go code for the first time we can quickly skim it to find the possible exit points and control flow points. I believe that is a highly valuable property of Go code. Using try
it becomes too easy to write code lacking that property.
I admit that it is likely that Go developers that value code readability would converge on usage idioms for try
that avoid these readability pitfalls. I hope that would happen since code readability seems to be a core value for many Go developers. But it’s not obvious to me that try
adds enough value over existing code idioms to carry the weight of adding a new concept to the language for everyone to learn and that can so easily harm readability.
I just would like to express that I think a bare try(foo())
actually bailing out of the calling function takes away from us the visual cue that function flow may change depending on the result.
I feel I can work with try
given enough getting used, but I also do feel we will need extra IDE support (or some such) to highlight try
to efficiently recognize the implicit flow in code reviews/debugging sessions
I actually really like this proposal. However, I do have one criticism. The exit point of functions in Go have always been marked by a return
. Panics are also exit points, however those are catastrophic errors that are typically not meant to ever be encountered.
Making an exit point of a function that isn’t a return
, and is meant to be commonplace, may lead to much less readable code. I had heard about this in a talk and it is hard to unsee the beauty of how this code is structured:
func 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)
}
}
This code may look like a big mess, and was meant to by the error handling draft, but let’s compare it to the same thing with try
.
func CopyFile(src, dst string) error {
defer func() {
err = fmt.Errorf("copy %s %s: %v", src, dst, err)
}()
r, err := try(os.Open(src))
defer r.Close()
w, err := try(os.Create(dst))
defer w.Close()
defer os.Remove(dst)
try(io.Copy(w, r))
try(w.Close())
return nil
}
You may look at this at first glance and think it looks better, because there is a lot less repeated code. However, it was very easy to spot all of the spots that the function returned in the first example. They were all indented and started with return
, followed by a space. This is because of the fact that all conditional returns must be inside of conditional blocks, thereby being indented by gofmt
standards. return
is also, as previously stated, the only way to leave a function without saying that a catastrophic error occurred. In the second example, there is only a single return
, so it looks like the only thing that the function ever should return is nil
. The last two try
calls are easy to see, but the first two are a bit harder, and would be even harder if they were nested somewhere, ie something like proc := try(os.FindProcess(try(strconv.Atoi(os.Args[1]))))
.
Returning from a function has seemed to have been a “sacred” thing to do, which is why I personally think that all exit points of a function should be marked by return
.
@deanveloper It is true that this proposal (and for that matter, any proposal trying to attempt the same thing) will remove explicitly visible return
statements from the source code - that is the whole point of the proposal after all, isn’t it? To remove the boilerplate of if
statements and returns
that are all the same. If you want to keep the return
’s, don’t use try
.
We are used to immediately recognize return
statements (and panic
’s) because that’s how this kind of control flow is expressed in Go (and many other languages). It seems not far fetched that we will also recognize try
as changing control flow after some getting used to it, just like we do for return
. I have no doubt that good IDE support will help with this as well.
One of the examples in the proposal nails the problem for me:
func printSum(a, b string) error {
fmt.Println(
"result:",
try(strconv.Atoi(a)) + try(strconv.Atoi(b)),
)
return nil
}
Control flow really becomes less obvious and very obscured.
This is also against the initial intention by Rob Pike that all errors need to be handled explicitly.
While a reaction to this can be “then don’t use it”, the problem is – other libraries will use it, and debugging them, reading them, and using them, becomes more problematic. This will motivate my company to never adopt go 2, and start using only libraries that don’t use try
. If I’m not alone with this, it might lead to a division a-la python 2⁄3.
…
Besides the name, I find awkward to have return
-like statement now hidden in the middle of expressions. This breaks Go flow style. It will make code reviews harder.
In general, I find that this proposal will only benefit to the lazy programmer who has now a weapon for shorter code and even less reason to make the effort of wrapping errors. As it will also make reviews harder (return in middle of expression), I think that this proposal goes against the “programming at scale” aim of Go.
…
What’s missing is an ergonomic way of this interacting with the error inspection proposal(s) on the table. I believe API’s should be very deliberate in what types of errors they return, and the default should probably be “returned errors are not inspectable in any way”. It should then be easy to go to a state where errors are inspectable in a precise way, as documented by the function signature (“It reports an error of kind X in circumstance A and an error of kind Y in circumstance B”).
Unfortunately, as of now, this proposal makes the most ergonomic option the most undesirable (to me); blindly passing through arbitrary error kinds. I think this is undesirable because it encourages not thinking about the kinds of errors you return and how users of your API will consume them. The added convenience of this proposal is certainly nice, but I fear it will encourage bad behavior because the perceived convenience will outweigh the perceived value of thinking carefully about what error information you provide (or leak).
A bandaid would be if errors returned by try
get converted into errors that are not “unwrappable”. Unfortunately this has pretty severe downsides as well, since it makes it so that any defer
could not inspect the errors itself. Additionally it prevents the usage where try
actually will return an error of a desirable kind (that is, use cases where try
is used carefully rather than carelessly).
We are used to immediately recognize return statements (and panic’s) because that’s how this kind of control flow is expressed in Go (and many other languages). It seems not far fetched that we will also recognize try as changing control flow after some getting used to it, just like we do for return. I have no doubt that good IDE support will help with this as well.
I think it is fairly far-fetched. In gofmt’ed code, a return always matches /^\t*return /
– it’s a very trivial pattern to spot by eye, without any assistance. try
, on the other hand, can occur anywhere in the code, nested arbitrarily deep in function calls. No amount of training will make us be able to immediately spot all control flow in a function without tool assistance.
Furthermore, a feature that depends on “good IDE support” will be at a disadvantage in all the environments where there is no good IDE support. Code review tools come to mind immediately – will Gerrit highlight all the try’s for me? What about people who choose not to use IDEs, or fancy code highlighting, for various reasons? Will acme start highlighting try
?
A language feature should be easy to understand on its own, not depend on editor support.
@dominikh asked:
Will acme start highlighting try?
So much about the try proposal is undecided, up in the air, unknown.
But this question I can answer definitively: no.
@dominikh try
always matches /try\(/
so I don’t know what your point is really. It’s equally as searchable and every editor I’ve ever heard of has a search feature.
@qrpnxz I think the point he was trying to make is not that you cannot search for it programatically, but that it’s harder to search for with your eyes. The regexp was just an analogy, with emphasis on the /^\t*
, signifying that all returns clearly stand out by being at the beginning of a line (ignoring leading whitespace).
This looks like a special cased macro.
…
panic
may be another flow controlling function, but it doesn’t return a value, making it effectively a statement. Compare this to try
, which is an expression and can occur anywhere.
recover
does have a value and affects flow control, but must occur in a defer
statement. These defer
s are typically function literals, recover
is only ever called once, and so recover
also effectively occurs as a statement. Again, compare this to try
which can occur anywhere.
I think those points mean that try
makes it significantly harder to follow control flow in a way that we haven’t had before, as has been pointed out before, but I didn’t see the distinction between statements and expressions pointed out.
I also want to point that try() is treated as expression eventhough it works as return statement. Yes, I know try is builtin macro but most of users will use this like functional programming, I guess.
func doSomething() (error, error, error, error, error) {
...
}
try(try(try(try(try(doSomething)))))
@mattn, I highly doubt any signficant number of people will write code like that.
…
2) Many people don’t like the idea of a built-in, or the function syntax that comes with it because it hides a return
. It would be better to use a keyword. (@sheerun, @Redundancy, @dolmen, @komuw, @RobertGrantEllis, @elagergren-spideroak). try
may also be easily overlooked (@peterbourgon), especially because it can appear in expressions that may be arbitrarily nested. @natefinch is concerned that try
makes it “too easy to dump too much in one line”, something that we usually try to avoid in Go. Also, IDE support to emphasize try
may not be sufficient (@dominikh); try
needs to “stand on its own”.
…
2) There is of course the possibility to use a keyword or special syntax instead of a built-in. A new keyword will not be backward-compatible. A new operator might, but seems even less visible. The detailed proposal discusses the various pros and cons at length. But perhaps we are misjudging this.
I do not like this approach, because of:
try()
function call interrupts code execution in the parent function.return
keyword, but the code actually returns.I like to emphasize a couple of things that may have been forgotten in the heat of the discussion:
1) The whole point of this proposal is to make common error handling fade into the background - error handling should not dominate the code. But should still be explicit. Any of the alternative suggestions that make the error handling sticking out even more are missing the point. As @ianlancetaylor already said, if these alternative suggestions do not reduce the amount of boilerplate significantly, we can just stay with the if
statements. (And the request to reduce boilerplate comes from you, the Go community.)
The whole point of this proposal is to make common error handling fade into the background - error handling should not dominate the code. But should still be explicit.
I like the idea of the comments listing try
as a statement. It’s explicit, still easy to gloss over (since it is of fixed length), but not so easy to gloss over (since it’s always in the same place) that they can be hidden away in a crowded line. It can also be combined with the defer fmt.HandleErrorf(...)
as noted before, however it does have the pitfall of abusing named parameters in order to wrap errors (which still seems like a clever hack to me. clever hacks are bad.)
One of the reasons I did not like try
as an expression is that it’s either too easy to gloss over, or not easy enough to gloss over. Take the following two examples:
// Too hidden, it's in a crowded function with many symbols that complicate the function.
f, err := os.OpenFile(try(FileName()), os.O_APPEND|os.O_WRONLY, 0600)
// Not easy enough, the word "try" already increases horizontal complexity, and it
// being an expression only encourages more horizontal complexity.
// If this code weren't collapsed to multiple lines, it would be extremely
// hard to read and unclear as to what's executing when.
fullContents := try(io.CopyN(
os.Stdout,
try(net.Dial("tcp", "localhost:"+try(buf.ReadString("\n"))),
try(strconv.Atoi(try(buf.ReadString("\n")))),
))
// easy to see while still not being too verbose
try name := FileName()
os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0600)
// does not allow for clever one-liners, code remains much more readable.
// also, the code reads in sequence from top-to-bottom.
try port := r.ReadString("\n")
try lengthStr := r.ReadString("\n")
try length := strconv.Atoi(lengthStr)
try con := net.Dial("tcp", "localhost:"+port)
try io.CopyN(os.Stdout, con, length)
This code is definitely contrived, I’ll admit. But what I am getting at is that, in general, try
as an expression doesn’t work well in:
1. The middle of crowded expressions that don’t need much error checking
2. Relatively simple multiline statements that require a lot of error checking
I however agree with @ianlancetaylor that beginning each line with try
does seem to get in the way of the important part of each statement (the variable being defined or the function being executed). However I think because it’s in the same location and is a fixed width, it is much easier to gloss over, while still noticing it. However, everybody’s eyes are different.
I also think encouraging clever one-liners in code is just a bad idea in general. I’m surprised that I could craft such a powerful one-liner as in my first example, it’s a snippet that deserves its own entire function because it is doing so much - but it fits on one line if I hadn’t of collapsed it to multiple for readability’s sake. All in one line:
fullContents := try(io.CopyN(os.Stdout, try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))), try(strconv.Atoi(try(r.ReadString("\n"))))))
It reads a port from a *bufio.Reader
, starts a TCP connection, and copies a number of bytes specified by the same *bufio.Reader
to stdout
. All with error handling. For a language with such strict coding conventions, I don’t think this should really even be allowed. I guess gofmt
could help with this, though.
@deanveloper
>
> fullContents := try(io.CopyN(os.Stdout, try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))), try(strconv.Atoi(try(r.ReadString("\n"))))))
>
I must admit not readable for me, I would probably feel I must:
fullContents := try(io.CopyN(os.Stdout,
try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))),
try(strconv.Atoi(try(r.ReadString("\n"))))))
or similar, for readability, and then we are back with a “try” at the beginning of every line, with indentation.
For a language with such strict coding conventions, I don’t think this should really even be allowed.
It is possible to write abominable code in Go. It is even possible to format it terribly; there are just strong norms and tools against it. Go even has goto
.
During code reviews, I sometimes ask people to break complicated expressions into multiple statements, with useful intermediate names. I would do something similar for deeply nested try
s, for the same reason.
Which is all to say: Let’s not try too hard to outlaw bad code, at the cost of distorting the language. We have other mechanisms for keeping code clean that are better suited for something that fundamentally involves human judgement on a case by case basis.
It is possible to write abominable code in Go. It is even possible to format it terribly; there are just strong norms and tools against it. Go even has goto.
During code reviews, I sometimes ask people to break complicated expressions into multiple statements, with useful intermediate names. I would do something similar for deeply nested trys, for the same reason.
Which is all to say: Let’s not try too hard to outlaw bad code, at the cost of distorting the language. We have other mechanisms for keeping code clean that are better suited for something that fundamentally involves human judgement on a case by case basis.
This is a good point. We shouldn’t outlaw a good idea just because it can be used to make bad code. However, I think that if we have an alternative that promotes better code, it may be a good idea. I really haven’t seen much talk against the raw idea behind try
as a statement (without all the else { ... }
junk) until @ianlancetaylor’s comment, however I may have just missed it.
Also, not everyone has code reviewers, some people (especially in the far future) will have to maintain unreviewed Go code. Go as a language normally does a very good job of making sure that almost all written code is well-maintainable (at least after a go fmt
), which is not a feat to overlook.
That being said, I am being awfully critical of this idea when it really isn’t horrible.
…
@josharian I’ve had this thought quite a bit recently. Go strives for pragmatism, not perfection, and its development frequently seems to be data-driven rather than principles-driven. You can write terrible Go, but it’s usually harder than writing decent Go (which is good enough for most people). Also worth pointing out — we have many tools to combat bad code: not just gofmt
and go vet
, but our colleagues, and the culture that this community has (very carefully) crafted to guide itself. I would hate to steer clear of improvements that help the general case simply because someone somewhere might footgun themselves.
To @josharian’s point earlier, I feel like a large part of the discussion about matching parentheses is mostly hypothetical and using contrived examples. I don’t know about you but I don’t find myself having a hard time writing function calls in my day-to-day programming. If I get to a point where an expression gets hard to read or comprehend, I divide it into multiple expressions using intermediary variables. I don’t see why try()
with function call syntax would be any different in this respect in practice.
@eandre Normally, functions do not have such a dynamic definition. Many forms of this proposal decrease safety surrounding the communication of control flow, and that’s troublesome.
…
A few people ran the logic in the opposite direction: people can write complex expressions, so they inevitably will, so you’d need IDE or other tool support to find the try expressions, so try is a bad idea. There are a few unsupported leaps here, though. The main one is the claim that because it is possible to write complex, unreadable code, such code will become ubiquitous. As @josharian noted, it is already “possible to write abominable code in Go.” That’s not commonplace because developers have norms about trying to find the most readable way to write a particular piece of code. So it is most certainly not the case that IDE support will be required to read programs involving try. And in the few cases where people write truly terrible code abusing try, IDE support is unlikely to be much use. This objection—people can write very bad code using the new feature—is raised in pretty much every discussion of every new language feature in every language. It is not terribly helpful. A more helpful objection would be of the form “people will write code that seems good at first but turns out to be less good for this unexpected reason,” like in the discussion of debugging prints.
Again: Readability must not be limited to people using specific tools. (I still print and read programs on paper, although people often give me weird looks for doing that.)
A few people ran the logic in the opposite direction: people can write complex expressions, so they inevitably will, so you’d need IDE or other tool support to find the try expressions, so try is a bad idea. There are a few unsupported leaps here, though. The main one is the claim that because it is possible to write complex, unreadable code, such code will become ubiquitous. As @josharian noted, it is already “possible to write abominable code in Go.” That’s not commonplace because developers have norms about trying to find the most readable way to write a particular piece of code. So it is most certainly not the case that IDE support will be required to read programs involving try. And in the few cases where people write truly terrible code abusing try, IDE support is unlikely to be much use. This objection—people can write very bad code using the new feature—is raised in pretty much every discussion of every new language feature in every language. It is not terribly helpful.
Isn’t this the entire reason Go doesn’t have a ternary operator?
Isn’t this the entire reason Go doesn’t have a ternary operator?
No. We can and should distinguish between “this feature can be used for writing very readable code, but may also be abused to write unreadable code” and “the dominant use of this feature will be to write unreadable code”.
Experience with C suggests that ? : falls squarely into the second category. (With the possible exception of min and max, I’m not sure I’ve ever seen code using ? : that was not improved by rewriting it to use an if statement instead. But this paragraph is getting off topic.)
@rsc
> […]
> The main one is the claim that because it is possible to write complex, unreadable code, such code will become ubiquitous. As @josharian noted, it is already “possible to write abominable code in Go.”
> […]
> go
> headRef := try(r.Head())
> parentObjOne := try(headRef.Peel(git.ObjectCommit))
> parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
> parentCommitOne := try(parentObjOne.AsCommit())
> parentCommitTwo := try(parentObjTwo.AsCommit())
>
I understand your position on “bad code” is that we can write awful code today like the following block.
parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())
What are your thoughts on disallowing nested try
calls so that we can’t accidentally write bad code?
If you disallow nested try
on the first version, you will be able to remove this limitation later if needed, it wouldn’t be possible the other way around.
I understand your position on “bad code” is that we can write awful code today like the following block.
parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit()) parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())
My position is Go developers do a decent job writing clear code and that almost certainly the compiler is not the only thing standing in the way of you or your coworkers writing code that looks like that.
What are your thoughts on disallowing nested
try
calls so that we can’t accidentally write bad code?
A large part of the simplicity of Go derives from the selection of orthogonal features that compose independently. Adding restrictions breaks orthogonality, composability, independence, and in doing so breaks the simplicity.
Today, it is a rule that if you have:
x := expression
y := f(x)
with no other use of x anywhere, then it is a valid program transformation to simplify that to
y := f(expression)
If we were to adopt a restriction on try expressions, then it would break any tool that assumed this was always a valid transformation. Or if you had a code generator that worked with expressions and might process try expressions, it would have to go out of its way to introduce temporaries to satisfy the restrictions. And so on and so on.
In short, restrictions add significant complexity. They need significant justification, not “let’s see if anyone bumps into this wall and asks us to take it down”.
I wrote a longer explanation two years ago at https://github.com/golang/go/issues/18130#issuecomment-264195616 (in the context of type aliases) that applies equally well here.
@rsc
We can and should distinguish between “this feature can be used for writing very readable code, but may also be abused to write unreadable code” and “the dominant use of this feature will be to write unreadable code”. Experience with C suggests that ? : falls squarely into the second category. (With the possible exception of min and max,
What first struck me about try()
— vs try
as a statement — was how similar it was in nestability to the ternary operator and yet how opposite the arguments for try()
and against ternary were (paraphrased):
try():
“You can nest it, but we doubt many will because most people want to write good code”,Respectfully, that rational for the difference between the two feels so subjective I would ask for some introspection and at least consider if you might be rationalizing a difference for feature you prefer vs. against a feature you dislike? #please_dont_shoot_the_messenger
“I’m not sure I’ve ever seen code using ? : that was not improved by rewriting it to use an if statement instead. But this paragraph is getting off topic.)”
In other languages I frequently improve statements by rewriting them from an if
to a ternary operator, e.g. from code I wrote today in PHP:
return isset( $_COOKIE[ CookieNames::CART_ID ] )
? intval( $_COOKIE[ CookieNames::CART_ID ] )
: null;
Compare to:
if ( isset( $_COOKIE[ CookieNames::CART_ID ] ) ) {
return intval( $_COOKIE[ CookieNames::CART_ID ] );
} else {
return null;
}
As far as I am concerned, the former is much improved over the latter.
#fwiw
return isset( $_COOKIE[ CookieNames::CART_ID ] )
? intval( $_COOKIE[ CookieNames::CART_ID ] )
: null;
Pretty sure this should be return intval( $_COOKIE[ CookieNames::CART_ID ] ) ?? null;
FWIW. 😁
@carl-mastrangelo
> “Pretty sure this should be return intval( $_COOKIE[ CookieNames::CART_ID ] ) ?? null;
You are assuming PHP 7.x. I was not. But then again, given your snarky face, you know that was not the point. :wink:
@mikeschinkel I’m not the Carl you’re looking for.
…
Nesting of function calls that return errors obscures the order of operations, and hinders debugging. The state of affairs when an error occurs, and therefore the call sequence, should be clear, but here it’s not:
try(step4(try(step1()), try(step3(try(step2())))))
Now recall that the language forbids:
f(t ? a : b)
and f(a++)
It would be trivial to return errors without context. A key rationale of check/handle was to encourage contextualization.
I see two problems with this: 1. It puts a LOT of code nested inside functions. That adds a lot of extra cognitive load, trying to parse the code in your head.
Number 2 I think is far worse. All the examples here are simple calls that return an error, but what’s a lot more insidious is this:
func doit(abc string) error {
a := fmt.Sprintf("value of something: %s\n", try(getValue(abc)))
log.Println(a)
return nil
}
This code can exit in the middle of that sprintf, and it’s going to be SUPER easy to miss that fact.
My vote is no. This will not make go code better. It won’t make it easier to read. It won’t make it more robust.
I’ve said it before, and this proposal exemplifies it - I feel like 90% of the complaints about Go are “I don’t want to write an if statement or a loop” . This removes some very simple if statements, but adds cognitive load and makes it easy to miss exit points for a function.
…
Maybe you don’t personally see this use case often, but many people do, and adding try
doesn’t really affect you. In fact, the rarer the use case is to you the less it affects you that try
is added.
@qrpnxz
f := try(os.Open("/foo"))
data := try(ioutil.ReadAll(f))
try(send(data))
(yes I understand there is ReadFile
and that this particular example is not the best way to copy data somewhere, not the point)
This takes more effort to read because you have to parse out the try’s inline. The application logic is wrapped up in another call.
I’d also argue that a defer
error handler here would not be good except to just wrap the error with a new message… which is nice but there is more to dealing with errors than making it easy for the human to read what happened.
In rust at least the operator is a postfix (?
added to the end of a call) which doesn’t place extra burden to dig out the the actual logic.
@cpuguy83
To me it is more readable with try
. In this example I read “open a file, read all bytes, send data”. With regular error handling I would read “open a file, check if there was an error, the error handling does this, then read all bytes, now check if somethings happened…” I know you can scan through the err != nil
s, but to me try
is just easier because when I see it I know the behaviour right away: returns if err != nil. If you have a branch I have to see what it does. It could do anything.
I’d also argue that a defer error handler here would not be good except to just wrap the error with a new message
I’m sure there are other things you can do in the defer, but regardless, try
is for the simple general case anyway. Anytime you want to do something more, there is always good ol’ Go error handling. That’s not going away.
…
No one is going to make you use try
.
https://github.com/golang/go/issues/32437#issuecomment-498908380
No one is going to make you use try.
Ignoring the glibness, I think that’s a pretty hand-wavy way to dismiss a design criticism.
Sure, I don’t have to use it. But anybody I write code with could use it and force me to try to decipher try(try(try(to()).parse().this)).easily())
. It’s like saying
No one is going to make you use the empty interface{}.
Anyway, Go’s pretty strict about simplicity: gofmt
makes all the code look the same way. The happy path keeps left and anything that could be expensive or surprising is explicit. try
as is proposed is a 180 degree turn from this. Simplicity != concise.
At the very least try
should be a keyword with lvalues.
No one is going to make you use try.
Ignoring the glibness, I think that’s a pretty hand-wavy way to dismiss a design criticism.
Sorry, glib was not my intent.
What I’m trying to say, is that try
is not intended to be a 100% solution. There are various error-handling paradigms that are not well-handled by try
. For instance, if you need to add callsite-dependent context to the error. You can always fall back to using if err != nil {
to handle those more complicated cases.
It is certainly a valid argument that try
can’t handle X, for various instances of X. But often handling case X means making the mechanism more complicated. There’s a tradeoff here, handling X on one hand but complicating the mechanism for everything else. What we do all depends on how common X is, and how much complication it would require to handle X.
So by “No one is going to make you use try”, I mean that I think the example in question is in the 10%, not the 90%. That assertion is certainly up for debate, and I’m happy to hear counterarguments. But eventually we’re going to have to draw the line somewhere and say “yeah, try
will not handle that case. You’ll have to use old-style error handling. Sorry.“.
It’s not that “try can’t handle this specific case of error handling” that’s the issue, it’s “try encourages you to not wrapping your errors”. The check-handle
idea forced you to write a return statement, so writing a error wrapping was pretty trivial.
Under this proposal you need to use a named return with a defer
, which is not intuitive and seems very hacky.
I mean that I think the example in question is in the 10%, not the 90%. That assertion is certainly up for debate, and I’m happy to hear counterarguments. But eventually we’re going to have to draw the line somewhere and say “yeah, try will not handle that case. You’ll have to use old-style error handling. Sorry.”.
Agreed, my opinion is that this line sould be drawn when checking for EOF or similar, not at wrapping. But maybe if errors had more context this wouldn’t be an issue anymore.
Could try()
auto-wrap errors with useful context for debugging? E.g. if xerrors
becomes errors
, errors should have a something that looks like a stack trace that try()
could add, no? If so maybe that would be enough 🤔
…
No one is going to make you use try.
That doesn’t mean it’s a good solution, I am making a point that the current idea has a flaw in the design and I’m asking for it to be addressed in a way that is less error prone.
I think examples like try(try(try(to()).parse().this)).easily())
are unrealistic, this could already be done with other functions and I think it would be fair for those reviewing the code to ask for it to be split.
@elagergren-spideroak hard to say that try
is annoying to see in one breath and then say that it’s not explicit in the next. You gotta pick one.
it is common to see function arguments being put into temporary variables first. I’m sure it would be more common to see
this := try(to()).parse().this
that := try(this.easily())
than your example.
try
doing nothing is the happy path, so that looks as expected. In the unhappy path all it does is return. Seeing there is a try
is enough to gather that information. There isn’t anything expensive about returning from a function either, so from that description I don’t think try
is doing a 180
The other problem I have with try
is that it makes it so much easier for people to dump more and logic into a single line. This is my major problem with most other languages, is that they make it really easy to put like 5 expressions in a single line, and I don’t want that for go.
this := try(to()).parse().this
that := try(this.easily())
^^ even this is downright awful. The first line, I have to jump back and forth doing paren matching in my head. Even the second line which is actually quite simple… is really hard to read.
Nested functions are hard to read. Period.
parser, err := to()
if err != nil {
return err
}
this := parser.parse().this
that, err := this.easily()
if err != nil {
return err
}
^^ This is so much easier and better IMO. It’s super simple and clear. yes, it’s a lot more lines of code, I don’t care. It’s very obvious.
@natefinch Absolutely agree.
I wonder if a rust style approach would be more palatable? Note this is not a proposal just thinking through it…
this := to()?.parse().this
that := this.easily()?
In the end I think this is nicer, but (could use a !
or something else too…), but still doesn’t fix the issue of handling errors well.
of course rust also has try()
pretty much just like this, but… the other rust style.
@cpuguy83
I wonder if a rust style approach would be more palatable?
The proposal presents an argument against this.
@natefinch agree. I think this is more geared towards improving the experience while writing Go instead of optimizing for reading. I wonder if IDE macros or snippets could solve the issue without this becoming a feature of the language.
…
This too was covered by a few, but I want to draw comparison between try()
and the continued request for ternary operators. Quoting from another Go team member’s comments about 18 months ago:
“when “programming in the large” (large code bases with large teams over long periods of time), code is read WAY more often than it’s written, so we optimize for readability, not writability.”
One of the primary stated reasons for not adding ternary operators is that they are hard to read and/or easy to misread when nested. Yet the same can be true of nested try()
statements like try(try(try(to()).parse().this)).easily())
.
Additional reasons for argue against ternary operators have been that they are “expressions” with that argument that nested expressions can add complexity. But does not try()
create a nestable expression too?
Now someone here said “I think examples like [nested try()
s] are unrealistic” and that statement was not challenged.
But if people accept as postulate that developers won’t nest try()
then why is the same deference not given to ternary operators when people say “I think deeply nested ternary operators are unrealistic?”
Bottom line for this point, I think if the argument against ternary operators are really valid, then they also should be considered valid arguments against this try()
proposal.
The thing I’m most concerned about is the need to have named return values just so that the defer statement is happy.
I think the overall error handling issue that the community complains about is a combination of the boilerplate of if err != nil
AND adding context to errors. The FAQ clearly states that the latter is left out intentionally as a separate problem, but I feel like then this becomes an incomplete solution, but I’ll be willing to give it a chance after thinking on these 2 things:
Declare err
at the beginning of the function.
Does this work? I recall issues with defer & unnamed results. If it doesn’t the proposal needs to consider this.
func sample() (string, error) {
var err error
defer fmt.HandleErrorf(&err, "whatever")
s := try(f())
return s, nil
}
wrapf
function that has the if err != nil
boilerplate.
go
func sample() (string, error) {
s, err := f()
try(wrapf(err, "whatever"))
return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
if err != nil {
// err = wrapped error
}
return err
}
If either work, I can deal with it.
func sample() (string, error) {
var err error
defer fmt.HandleErrorf(&err, "whatever")
s := try(f())
return s, nil
}
This will not work. The defer will update the local err
variable, which is unrelated to the return value.
func sample() (string, error) {
s, err := f()
try(wrapf(err, "whatever"))
return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
if err != nil {
// err = wrapped error
}
return err
}
That should work. It will call wrapf even on a nil error, though. This will also (continue to) work, and is IMO a lot clearer:
func sample() (string, error) {
s, err := f()
if err != nil {
return "", wrap(err)
}
return s, nil
}
No one is going to make you use try
.
Not sure why anyone ever would write a function like this but what would be the envisioned output for
try(foobar())
If
foobar
returned(error, error)
Why would you return more than one error from a function? If you are returning more than one error from function, perhaps function should be split into two separate ones in the first place, each returning just one error.
Could you elaborate with an example?
…
@Goodwine: As @randall77 already pointed out, your first suggestion won’t work. One option we have thought about (but not discussed in the doc) is the possibility of having some predeclared variable that denotes the error
result (if one is present in the first place). That would eliminate the need for naming that result just so it can be used in a defer
. But that would be even more magic; it doesn’t seem justified. The problem with naming the return result is essentially cosmetic, and where that matters most is in the auto-generated APIs served by go doc
and friends. It would be easy to address this in those tools (see also the detailed design doc’s FAQ on this subject).
I share the two concerns raised by @buchanae, re: named returns and contextual errors.
I find named returns a bit troublesome as it is; I think they are only really beneficial as documentation. Leaning on them more heavily is a worry. Sorry to be so vague, though. I’ll think about this more and provide some more concrete thoughts.
The reason we are skeptical about calling try() may be two implicit binding. We can not see the binding for the return value error and arguments for try(). For about try(), we can make a rule that we must use try() with argument function which have error in return values. But binding to return values are not. So I’m thinking more expression is required for users to understand what this code doing.
func doSomething() (int, %error) {
f := try(foo())
...
}
%error
in return values.It is hard to add new requirements/feature to the existing syntax.
To be honest, I think that foo() should also have %error.
Add 1 more rule
…
defer
corner cases is not only awful for things like godoc, but most importantly it’s very error prone. I don’t care I can wrap the whole thing with another func()
to go around the issue, it’s just more things I need to keep in mind, I think it encourages a “bad practice”.…
defer
is used but, having said that, I can’t think of any other solution which wouldn’t be at odds with the way the rest of the language works. So I think we will just have to accept this if the proposal is adopted.…
1) If the only issue with try
is the fact that one will have to name an error return so we can decorate an error via defer
, I think we are good. If naming the result turns out to be a real issue, we could address it. A simple mechanism I can think of would be a predeclared variable that is an alias to an error result (think of it as holding the error which triggered the most recent try
). There may be better ideas. We didn’t propose this because there is already a mechanism in the language, which is to name the result.
I like this from @jargv:
Since try() is already magical, and aware of the error return value, could it be augmented to also return a pointer to that value in when called in the nullary (zero argument) form? That would eliminate the need for named returns
But instead of overloading the name try
based on the number of args, I think there could be another magic builtin, say reterr
or something.
I’m mostly in favor of this proposal.
My main concern, shared with many commenters, is about named result parameters. The current proposal certainly encourages much more use of named result parameters and I think that would be a mistake. I don’t believe this is simply a matter of style as the proposal states: named results are a subtle feature of the language which, in many cases, makes the code more bug-prone or less clear. After ~8 years of reading and writing Go code, I really only use named result parameters for two purposes:
error
) inside a defer…
5) There’s some concern about the use of named returns (@buchanae, @adg).
…
5) Named return values: The detailed document discusses this at length. If this is the main concern about this proposal then we’re in a good spot, I think.
Since try()
is already magical, and aware of the error return value, could it be augmented to also return a pointer to that value in when called in the nullary (zero argument) form? That would eliminate the need for named returns, and I believe, help to visually correlate where the error is expected to come from in defer statements. For example:
func foo() error {
defer fmt.HandleErrorf(try(), "important foo context info")
try(bar())
try(baz())
try(etc())
}
@jargv Thanks for your suggestion. This is an interesting idea (see also my comment here on this subject). To summarize:
- try(x)
operates as proposed
- try()
returns an *error
pointing to the error result
This would be indeed another way to get to the result w/o having to name it.
@cespare The suggestion by @jargv looks much simpler to me than what you are proposing. It solves the same problem of access to the result error. What do you think?
The main drawback of this approach is that the error result parameter needs to be named, possibly leading to less pretty APIs (but see the FAQs on this subject). We believe that we will get used to it once this style has established itself.
Not sure if something like this have been suggested before, can’t find it here or in the proposal. Have you considered another builtin function that returns a pointer to the current function’s error return value? eg:
func example() error {
var err *error = funcerror() // always return a non-nil pointer
fmt.Print(*err) // always nil if the return parameters are not named and not in a defer
defer func() {
err := funcerror()
fmt.Print(*err) // "x"
}
return errors.New("x")
}
func exampleNamed() (err error) {
funcErr := funcerror()
fmt.Print(*funcErr) // "nil"
err = errors.New("x")
fmt.Print(*funcErr) // "x", named return parameter is reflected even before return is called
*funcErr = errors.New("y")
fmt.Print(err) // "y", unfortunate side effect?
defer func() {
funcErr := funcerror()
fmt.Print(*funcErr) // "z"
fmt.Print(err) // "z"
}
return errors.New("z")
}
usage with try:
func CopyFile(src, dst string) (error) {
defer func() {
err := funcerror()
if *err != nil {
*err = fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}()
// one liner alternative
// defer fmt.HandleErrorf(funcerror(), "copy %s %s", src, dst)
r := try(os.Open(src))
defer r.Close()
w := try(os.Create(dst))
defer func() {
w.Close()
err := funcerror()
if *err != nil {
os.Remove(dst) // only if a “try” fails
}
}()
try(io.Copy(w, r))
try(w.Close())
return nil
}
Alternatively funcerror (the name is a work in progress :D ) could return nil if not called inside defer.
Another alternative is that funcerror returns an “Errorer” interface to make it read-only:
type interface Errorer() {
Error() error
}
Here’s another concern with using defers for error handling.
try
is a controlled/intended exit from a function. defers run always, including uncontrolled/unintended exits from functions. That mismatch could cause confusion. Here’s an imaginary scenario:
func someHTTPHandlerGuts() (err error) {
defer func() {
recordMetric("db call failed")
return fmt.HandleErrorf("db unavailable: %v", err)
}()
data := try(makeDBCall)
// some code that panics due to a bug
return nil
}
Recall that net/http recovers from panics, and imagine debugging a production issue around the panic. You’d look at your instrumentation and see a spike in db call failures, from the recordMetric
calls. This might mask the true issue, which is the panic in the subsequent line.
I’m not sure how serious a concern this is in practice, but it is (sadly) perhaps another reason to think that defer is not an ideal mechanism for error handling.
@josharian Thinking about the interaction with panic
is important here, and I’m glad you brought it up, but your example seems strange to me. In the following code it doesn’t make sense to me that the defer always records a "db call failed"
metric. It would be a false metric if someHTTPHandlerGuts
succeeds and returns nil
. The defer
runs in all exit cases, not just error or panic cases, so the code seems wrong even if there is no panic.
func someHTTPHandlerGuts() (err error) {
defer func() {
recordMetric("db call failed")
return fmt.HandleErrorf("db unavailable: %v", err)
}()
data := try(makeDBCall)
// some code that panics due to a bug
return nil
}
…
Suggestion everyone will hate for an implicit name for a final error return variable: $err
. It’s better than try()
IMO. :-)
Is using try()
with zero args (or a different builtin) still up for consideration or has this been ruled out.
After the changes to the proposal I’m still concerned how it makes the use of named return values more “common”. However I don’t have data to back that up :upside_down_face:.
If try()
with zero args (or a different builtin) is added to the proposal, could the examples in the proposal be updated to use try()
(or a different builtin) to avoid named returns?
@Goodwine Nobody has ruled out try()
to get to the error value; though if something like this is needed, it may be better to have a predeclared err
variable as @rogpeppe suggested (I think).
Again, this proposal doesn’t rule any of this out. Let’s go there if we find out it is necessary.
Thanks for all the clarification. The more i think the more i like the proposal and see how it fit the goals.
Why not use a function like recover()
instead of err
that we don’t know from where it come ? It would be more consistent and maybe easier to implement.
func f() error {
defer func() {
if err:=error();err!=nil {
...
}
}()
}
edit: I never use named return, then it’ll be strange for me to add named return just for this
@flibustenet, see also https://swtch.com/try.html#named for a few similar suggestions. (Answering all of them: we could do that, but it’s not strictly necessary given named results, so we might as well try to use the existing concept before deciding we need to provide a second way.)
…
Secondly, try
is arguably a fair solution to most of the problems underlying https://github.com/golang/go/issues/19642. To take an example from that issue, you could use try
to avoid writing out all the return values each time. This is also potentially useful when returning by-value struct types with long names.
func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
xx := int(x)
if f.NumGlyphs() <= xx {
try(ErrNotFound)
}
i := f.cached.locations[xx+0]
j := f.cached.locations[xx+1]
if j < i {
try(errInvalidGlyphDataLength)
}
if j-i > maxGlyphDataLength {
try(errUnsupportedGlyphDataLength)
}
buf, err = b.view(&f.src, int(i), int(j-i))
return buf, i, j - i, err
}
In my opinion, using try
to avoid writing out all the return values is actually just another strike against it.
func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
xx := int(x)
if f.NumGlyphs() <= xx {
try(ErrNotFound)
}
//...
I completely understand the desire to avoid having to write out return nil, 0, 0, ErrNotFound
, but I would much rather solve that some other way.
The word try
doesn’t mean “return”. And that’s how it’s being used here. I would actually prefer that the proposal change so that try
can’t take an error
value directly, because I don’t ever want anyone writing code like that ^^ . It reads wrong. If you showed that code to a newbie, they’d have no clue what that try was doing.
If we want a way to easily just return defaults and an error value, let’s solve that separately. Maybe another builtin like
return default(ErrNotFound)
At least that reads with some kind of logic.
But let’s not abuse try
to solve some other problem.
@natefinch if the try
builtin is named check
like in the original proposal, it would be check(err)
which reads considerably better, imo.
Putting that aside, I don’t know if it’s really an abuse to write try(err)
. It falls out of the definition cleanly. But, on the other hand, that also means that this is legal:
a, b := try(1, f(), err)
I guess my main problem with try
is that it’s really just a panic
that only goes up one level… except that unlike panic, it’s an expression, not a statement, so you can hide it in the middle of a statement somewhere. That almost makes it worse than panic.
@natefinch If you conceptualize it like a panic that goes up one level and then does other things, it seems pretty messy. However, I conceptualize it differently. Functions that return errors in Go are effectively returning a Resulttry
is a utility that unpacks the result and either returns an “error result” if error != nil
, or unpacks the T portion of the result if error == nil
.
Of course, in Go we don’t actually have result objects, but it’s effectively the same pattern and try
seems like a natural codification of that pattern. I believe that any solution to this problem is going to have to codify some aspect of error handling, and try
s take on it seems reasonable to me. Myself and others are suggesting to extend the capability of try
a bit to better fit existing Go error handling patterns, but the underlying concept remains the same.
…
2) One of the complaints about the current proposal is the need to name the error result in order to get access to it. Any alternative proposal will have the same problem unless the alternative introduces extra syntax, i.e., more boilerplate (such as ... else err { ... }
and the like) to explicitly name that variable. But what is interesting: If we don’t care about decorating an error and do not name the result parameters, but still require an explicit return
because there’s an explicit handler of sorts, that return
statement will have to enumerate all the (typically zero) result values since a naked return is not permitted in this case. Especially if a function does a lot of error returns w/o decorating the error, those explicit returns (return nil, err
, etc.) add to the boilerplate. The current proposal, and any alternative that doesn’t require an explicit return
does away with that. On the other hand, if one does want to decorate the error, the current proposal requires that one name the error result (and with that all the other results) to get access to the error value. This has the nice side effect that in an explicit handler one can use a naked return and does not have to repeat all the other result values. (I know there are some strong feelings about naked returns, but the reality is that when all we care about is the error result, it’s a real nuisance to have to enumerate all the other (typically zero) result values - it adds nothing to the understanding of the code). In other words, having to name the error result so that it can be decorated enables further reduction of boilerplate.
@griesemer My problem with your proposed use of defer as a way to handle the error wrapping is that the behavior from the snippet I showed (repeated below) is not very common AFAICT, and because it’s very rare then I can imagine people writing this thinking it works when it doesn’t.
Like.. a beginner wouldn’t know this, if they have a bug because of this they won’t go “of course, I need a named return”, they would get stressed out because it should work and it doesn’t.
var err error
defer fmt.HandleErrorf(err);
try
is already too magic so you may as well go all the way and add that implicit error value. Think on the beginners, not on those who know all the nuances of Go. If it’s not clear enough, I don’t think it’s the right solution.
Or… Don’t suggest using defer like this, try another way that’s safer but still readable.
@Goodwine Point taken. The reason for not providing more direct error handling support is discussed in the design doc in detail. It is also a fact that over the course of a year or so (since the draft designs published at last year’s Gophercon) no satisfying solution for explicit error handling has come up. Which is why this proposal leaves this out on purpose (and instead suggests to use a defer
). This proposal still leaves the door open for future improvements in that regard.
I don’t follow this line:
defer fmt.HandleErrorf(&err, “foobar”)
It drops the inbound error on the floor, which is unusual. Is it meant to be used something more like this?
defer fmt.HandleErrorf(&err, “foobar: %v”, err)
The duplication of err is a bit stutter-y. This is not really directly apropos to the proposal, just a side comment about the doc.
…
Regarding the “foobar” example: It’s just a bad example. I should probably change it. Thanks for bringing it up.
defer fmt.HandleErrorf(&err, “foobar: %v”, err)
Actually, that can’t be right, because err
will be evaluated too early. There are a couple of ways around this, but none of them as clean as the original (I think flawed) HandleErrorf. I think it’d be good to have a more realistic worked example or two of a helper function.
EDIT: this early evaluation bug is present in an example near the end of the doc:
defer fmt.HandleErrorf(&err, "copy %s %s: %v", src, dst, err)
@josharian Regarding your comment in https://github.com/golang/go/issues/32437#issuecomment-498941854 , I don’t think there is an early evaluation error here.
defer fmt.HandleErrorf(&err, “foobar: %v”, err)
The unmodified value of err
is passed to HandleErrorf
, and a pointer to err
is passed. We check whether err
is nil
(using the pointer). If not, we format the string, using the unmodified value of err
. Then we set err
to the formatted error value, using the pointer.
@ianlancetaylor, I think @josharian is correct: the “unmodified” value of err
is the value at the time the defer
is pushed onto the stack, not the (presumably intended) value of err
set by try
before returning.
@bcmills @josharian Ah, of course, thanks. So it would have to be
defer func() { fmt.HandleErrorf(&err, “foobar: %v”, err) }()
Not so nice. Maybe fmt.HandleErrorf
should implicitly pass the error value as the last argument after all.
@ianlancetaylor if fmt.HandleErrorf
sends err as the first argument after format then the implementation will be nicer and user will be able to reference it by %[1]v
always.
The problem that @josharian pointed out can be avoided by delaying the evaluation of err:
defer func() { fmt.HandleErrorf(&err, "oops: %v", err) }()
Doesn’t look great, but it should work. I’d prefer however if this can be addressed by adding a new formatting verb/flag for error pointers, or maybe for pointers in general, that prints the dereferenced value as with plain %v
. For the purpose of the example, let’s call it %*v
:
defer fmt.HandleErrorf(&err, "oops: %*v", &err)
The snag aside, I think that this proposal looks promising, but it seems crucial to keep the ergonomics of adding context to errors in check.
Edit:
Another approach is to wrap the error pointer in a struct that implements Stringer
:
type wraperr struct{ err *error }
func (w wraperr) String() string { return (*w.err).Error() }
…
defer handleErrorf(&err, "oops: %v", wraperr{&err})
If defer-based error handling is going to be A Thing, then something like this should probably be added to the errors package:
f := try(os.Create(filename))
defer errors.Deferred(&err, f.Close)
Ignoring the errors of deferred Close statements is a pretty common issue. There should be a standard tool to help with it.
Thinking about it more, there should be a couple of common helper functions. Perhaps they should be in a package called “deferred”.
Addressing the proposal for a check
with format to avoid naming the return, you can just do that with a function that checks for nil, like so
func Format(err error, message string, args ...interface{}) error {
if err == nil {
return nil
}
return fmt.Errorf(...)
}
This can be used without a named return like so:
func foo(s string) (int, error) {
n, err := strconv.Atoi(s)
try(deferred.Format(err, "bad string %q", s))
return n, nil
}
The proposed fmt.HandleError could be put into the deferred package instead and my errors.Defer helper func could be called deferred.Exec
and there could be a conditional exec for procedures to execute only if the error is non-nil.
Putting it together, you get something like
func CopyFile(src, dst string) (err error) {
defer deferred.Annotate(&err, "copy %s %s", src, dst)
r := try(os.Open(src))
defer deferred.Exec(&err, r.Close)
w := try(os.Create(dst))
defer deferred.Exec(&err, r.Close)
defer deferred.Cond(&err, func(){ os.Remove(dst) })
try(io.Copy(w, r))
return nil
}
Another example:
func (p *pgStore) DoWork() (err error) {
tx := try(p.handle.Begin())
defer deferred.Cond(&err, func(){ tx.Rollback() })
var res int64
err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
try(deferred.Format(err, "insert table")
_, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
try(deferred.Format(err, "insert table2"))
return tx.Commit()
}
…
wrapf
function that has the if err != nil
boilerplate.
go
func sample() (string, error) {
s, err := f()
try(wrapf(err, "whatever"))
return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
if err != nil {
// err = wrapped error
}
return err
}
If either work, I can deal with it.Building on @Goodwine’s impish point, you don’t really need separate functions like HandleErrorf
if you have a single bridge function like
func handler(err *error, handle func(error) error) {
// nil handle is treated as the identity
if *err != nil && handle != nil {
*err = handle(*err)
}
}
which you would use like
defer handler(&err, func(err error) error {
if errors.Is(err, io.EOF) {
return nil
}
return fmt.Errorf("oops: %w", err)
})
You could make handler
itself a semi-magic builtin like try
.
If it’s magic, it could take its first argument implicitly—allowing it to be used even in functions that don’t name their error
return, knocking out one of the less fortunate aspects of the current proposal while making it less fussy and error prone to decorate errors. Of course, that doesn’t reduce the previous example by much:
defer handler(func(err error) error {
if errors.Is(err, io.EOF) {
return nil
}
return fmt.Errorf("oops: %w", err)
})
If it were magic in this way, it would have to be a compile time error if it were used anywhere except as the argument to defer
. You could go a step farther and make it implicitly defer, but defer handler
reads quite nicely.
Since it uses defer
it could call its handle
func whenever there was a non-nil error being returned, making it useful even without try
since you could add a
defer handler(wrapErrWithPackageName)
at the top to fmt.Errorf("mypkg: %w", err)
everything.
That gives you a lot of the older check
/handle
proposal but it works with defer naturally (and explicitly) while getting rid of the need, in most cases, to explicitly name an err
return. Like try
it’s a relatively straightforward macro that (I imagine) could be implemented entirely in the front end.
In the case decorating errors
func myfunc()( err error){
try(thing())
defer func(){
err = errors.Wrap(err,"more context")
}()
}
This feels considerably more verbose and painful than the existing paradigms, and not as concise as check/handle. The non-wrapping try() variant is more concise, but it feels like people will end up using a mix of try, and plain error returns. I’m not sure I like the idea of mixing try and simple error returns, but I’m totally sold on decorating errors (and looking forward to Is/As). Make me think that whilst this is syntactically neat, I’m not sure I would want to actually use it. check/handle felt something I would more thoroughly embrace.
…
The preference to overload the obvious and straightforward nature of defer
as alarming. If I write defer closeFile(f)
that is straightforward and obvious to me what is happening and why; at the end of the func that will be called. And while using defer
for panic()
and recover()
is less obvious, I rarely if ever use it and almost never see it when reading other’s code.
Spoo to overload defer
to also handle errors is not obvious and confusing. Why the keyword defer
? Does defer
not mean “Do later” instead of “Maybe to later?”
Also there is the concern mentioned by the Go team about defer
performance. Given that, it seems doubly unfortunate that defer
is being considered for the “hot path” code flow.
Defers
The primary change from the Gophercon check/handle draft to this proposal was dropping handle
in favor of reusing defer
. Now error context would be added by code like this deferred call (see my earlier comment about error context):
func CopyFile(src, dst string) (err error) {
defer HelperToBeNamedLater(&err, "copy %s %s", src, dst)
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
...
}
The viability of defer as the error annotation mechanism in this example depends on a few things.
Named error results. There has been a lot of concern about adding named error results. It is true that we have discouraged that in the past where not needed for documentation purposes, but that is a convention we picked in the absence of any stronger deciding factor. And even in the past, a stronger deciding factor like referring to specific results in the documentation outweighed the general convention for unnamed results. Now there is a second stronger deciding factor, namely wanting to refer to the error in a defer. That seems like it should be no more objectionable than naming results for use in documentation. A number of people have reacted quite negatively to this, and I honestly don’t understand why. It almost seems like people are conflating returns without expression lists (so-called “naked returns”) with having named results. It is true that returns without expression lists can lead to confusion in larger functions. Avoiding that confusion by avoiding those returns in long functions often makes sense. Painting named results with the same brush does not.
Address expressions. A few people have raised concerns that using this pattern will require Go developers to understand address-of expressions. Storing any value with pointer methods into an interface already requires that, so this does not seem like a significant drawback.
Defer itself. A few people have raised concerns about using defer as a language concept at all, again because new users might be unfamiliar with it. Like with address expressions, defer is a core language concept that must be learned eventually. The standard idioms around things like defer f.Close()
and defer l.mu.Unlock()
are so common that it is hard to justify avoiding defer as an obscure corner of the language.
Performance. We have discussed for years working on making common defer patterns like a defer at the top of a function have zero overhead compared to inserting that call by hand at each return. We think we know how to do that and will explore it for the next Go release. Even if not, though, the current overhead of approximately 50 ns should not be prohibitive for most calls that need to add error context. And the few performance-sensitive calls can continue to use if statements until defer is faster.
The first three concerns all amount to objections to reusing existing language features. But reusing existing language features is exactly the advance of this proposal over check/handle: there is less to add to the core language, fewer new pieces to learn, and fewer surprising interactions.
Still, we appreciate that using defer this way is new and that we need to give people time to evaluate whether defer works well enough in practice for the error handling idioms they need.
Since we kicked off this discussion last August I’ve been doing the mental exercise of “how would this code look with check/handle?” and more recently “with try/defer?” each time I write new code. Usually the answer means I write different, better code, with the context added in one place (the defer) instead of at every return or omitted altogether.
Given the idea of using a deferred handler to take action on errors, there are a variety of patterns we could enable with a simple library package. I’ve filed #32676 to think more about that, but using the package API in that issue our code would look like:
func CopyFile(src, dst string) (err error) {
defer errd.Add(&err, "copy %s %s", src, dst)
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
...
}
If we were debugging CopyFile and wanted to see any returned error and stack trace (similar to wanting to insert a debug print), we could use:
func CopyFile(src, dst string) (err error) {
defer errd.Trace(&err)
defer errd.Add(&err, "copy %s %s", src, dst)
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
...
}
and so on.
Using defer in this way ends up being fairly powerful, and it retains the advantage of check/handle that you can write “do this on any error at all” once at the top of the function and then not worry about it for the rest of the body. This improves readability in much the same way as early quick exits.
Will this work in practice? That’s an important open question. We want to find out.
Having done the mental experiment of what defer would look like in my own code for a few months, I think it is likely to work. But of course getting to use it in real code is not always the same. We will need to experiment to find out.
People can experiment with this approach today by continuing to write if err != nil
statements but copying the defer helpers and making use of them as appropriate. If you are inclined to do this, please let us know what you learn.
@rsc if, later, handlers are added to try
will there be an errors/errc package with functions like func Wrap(msg string) func(error) error
so you can do try(f(), errc.Wrap("f failed"))
?
@rsc here is my insight as to why I personally don’t like the defer HandleFunc(&err, ...)
pattern. It’s not because I associate it with naked returns or anything, it just feels too “clever”.
There was an error handling proposal a few months (maybe a year?) ago, however I have lost track of it now. I forgot what it was requesting, however someone had responded with something along the lines of:
func myFunction() (i int, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("wrapping the error: %s", err)
}
}()
// ...
return 0, err
// ...
return someInt, nil
}
It was interesting to see to say the least. It was my first time seeing defer
used for error handling, and now it is being shown here. I see it as “clever” and “hacky”, and, at least in the example I bring up, it doesn’t feel like Go. However, wrapping it in a proper function call with something like fmt.HandleErrorf
does help it feel much nicer. I still feel negatively towards it, though.
Another reason I can see people not liking it is that when one writes return ..., err
, it looks like err
should be returned. But it doesn’t get returned, instead the value is modified before sending. I have said before that return
has always seemed like a “sacred” operation in Go, and encouraging code that modifies a returned value before actually returning just feels wrong.
I have two concerns: - named returns have been very confusing, and this encourages them with a new and important use case - this will discourage adding context to errors
In my experience, adding context to errors immediately after each call site is critical to having code that can be easily debugged. And named returns have caused confusion for nearly every Go developer I know at some point.
A more minor, stylistic concern is that it’s unfortunate how many lines of code will now be wrapped in try(actualThing())
. I can imagine seeing most lines in a codebase wrapped in try()
. That feels unfortunate.
I think these concerns would be addressed with a tweak:
a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)
check()
would behave much like try()
, but would drop the behavior of passing through function return values generically, and instead would provide the ability to add context. It would still trigger a return.
This would retain many of the advantages of try()
:
- it’s a built-in
- it follows the existing control flow WRT to defer
- it aligns with existing practice of adding context to errors well
- it aligns with current proposals and libraries for error wrapping, such as errors.Wrap(err, "context message")
- it results in a clean call site: there’s no boilerplate on the a, b, err := myFunc()
line
- describing errors with defer fmt.HandleError(&err, "msg")
is still possible, but doesn’t need to be encouraged.
- the signature of check
is slightly simpler, because it doesn’t need to return an arbitrary number of arguments from the function it is wrapping.
@buchanae interesting. As written, though, it moves fmt-style formatting from a package into the language itself, which opens up a can of worms.
As written, though, it moves fmt-style formatting from a package into the language itself, which opens up a can of worms.
Good point. A simpler example:
a, b, err := myFunc()
check(err, "calling myFunc")
@buchanae We have considered making explicit error handling more directly connected with try
- please see the detailed design doc, specifically the section on Design iterations. Your specific suggestion of check
would only allow to augment errors through something like a fmt.Errorf
like API (as part of the check
), if I understand correctly. In general, people may want to do all kinds of things with errors, not just create a new one that refers to the original one via its error string.
Again, this proposal does not attempt to solve all error handling situations. I suspect in most cases try
makes sense for code that now looks basically like this:
a, b, c, ... err := try(someFunctionCall())
if err != nil {
return ..., err
}
There is an awful lot of code that looks like this. And not every piece of code looking like this needs more error handling. And where defer
is not right, one can still use an if
statement.
people may want to do all kinds of things with errors, not just create a new one that refers to the original one via its error string.
try
doesn’t attempt to handle all the kinds of things people want to do with errors, only the ones that we can find a practical way to make significantly simpler. I believe my check
example walks the same line.
…
I do think there is a real concern that people will strive to structure their code so that try
can be used, and therefore avoid adding context to errors. This is a particularly weird time to introduce this, given we’re just now providing better ways to add context to errors through official error wrapping features.
I agree with some of the concerns raised above regarding adding context to an error. I am slowly trying to shift from just returning an error to always decorate it with a context and then returning it. With this proposal, I will have to completely change my function to use named return params (which I feel is odd because I barely use naked returns).
As @griesemer says:
Again, this proposal does not attempt to solve all error handling situations. I suspect in most cases try makes sense for code that now looks basically like this: a, b, c, … err := try(someFunctionCall()) if err != nil { return …, err } There is an awful lot of code that looks like this. And not every piece of code looking like this needs more error handling. And where defer is not right, one can still use an if statement.
Yes, but shouldn’t good, idiomatic code always wrap/decorate their errors ? I believe that’s why we are introducing refined error handling mechanisms to add context/wrap errors in stdlib. As I see, this proposal only seems to consider the most basic use case.
Moreover, this proposal addresses only the case of wrapping/decorating multiple possible error return sites at a single place, using named parameters with a defer call.
But it doesn’t do anything for the case when one needs to add different contexts to different errors in a single function. For eg, it is very essential to decorate the DB errors to get more information on where they are coming from (assuming no stack traces)
This is an example of a real code I have -
func (p *pgStore) DoWork() error {
tx, err := p.handle.Begin()
if err != nil {
return err
}
var res int64
err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
if err != nil {
tx.Rollback()
return fmt.Errorf("insert table: %w", err)
}
_, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
if err != nil {
tx.Rollback()
return fmt.Errorf("insert table2: %w", err)
}
return tx.Commit()
}
According to the proposal:
If error augmentation or wrapping is desired there are two approaches: Stick with the tried-and-true if statement, or, alternatively, “declare” an error handler with a defer statement:
I think this will fall into the category of “stick with the tried-and-true if statement”. I hope the proposal can be improved to address this too.
…
It would be trivial to return errors without context. A key rationale of check/handle was to encourage contextualization.
It’s tied to type error
and the last return value. If we need to inspect other return values/types for exceptional state, we’re back to: if errno := f(); errno != 0 { ... }
…
To me, error handling is one of the most important parts of a code base.
Already too much go code is if err != nil { return err }
, returning an error from deep in the stack without adding extra context, or even (possibly) worse adding context by masking the underlying error with fmt.Errorf
wrapping.
Providing a new keyword that is kind of magic that does nothing but replace if err != nil { return err }
seems like a dangerous road to go down.
Now all code will just be wrapped in a call to try. This is somewhat fine (though readability sucks) for code that is dealing with only in-package errors such as:
func foo() error {
/// stuff
try(bar())
// more stuff
}
But I’d argue that the given example is really kind of horrific and basically leaves the caller trying to understand an error that is really deep in the stack, much like exception handling. Of course, this is all up to the developer to do the right thing here, but it gives the developer a great way to not care about their errors with maybe a “we’ll fix this later” (and we all know how that goes).
I wish we’d look at the issue from a different perspective than *“how can we reduce repetition” and more about “how can we make (proper) error handling simpler and developers more productive”. We should be thinking about how this will affect running production code.
*Note: This doesn’t actually reduce repetition, just changes what’s being repeated, all the while making the code less readable because everything is encased in a try()
.
One last point: Reading the proposal at first it seems nice, then you start to get into all the gotchas (at least the ones listed) and it’s just like “ok yeah this is too much”.
I realize much of this is subjective, but it’s something I care about. These semantics are incredibly important. What I want to see is a way to make writing and maintaining production level code simpler such that you might as well do errors “right” even for POC/demo level code.
@cpuguy83 If you read the proposal you would see there is nothing preventing you from wrapping the error. In fact there are multiple ways of doing it while still using try
. Many people seem to assume that for some reason though.
if err != nil { return err }
is equally as “we’ll fix this later” as try
except more annoying when prototyping.
I don’t know how things being inside of a pair of parenthesis is less readable than function steps being every four lines of boilerplate either.
It’d be nice if you pointed out some of these particular “gotchas” that bothered you since that’s the topic.
…
What if I have 3 places that can error-out and I want to wrap each place separately? try()
makes this very hard, in fact try()
is already discouraging wrapping errors given the difficulty of it, but here is an example of what I mean:
func before() error {
x, err := foo()
if err != nil {
wrap(err, "error on foo")
}
y, err := bar(x)
if err != nil {
wrapf(err, "error on bar with x=%v", x)
}
fmt.Println(y)
return nil
}
func after() (err error) {
defer fmt.HandleErrorf(&err, "something failed but I don't know where: %v", err)
x := try(foo())
y := try(bar(x))
fmt.Println(y)
return nil
}
try
. Then you gain nothing from try
, but you also don’t lose anything.Let’s say it’s a good practice to wrap errors with useful context, try()
would be considered a bad practice because it’s not adding any context. This means that try()
is a feature nobody wants to use and become a feature that’s used so rarely that it may as well not have existed.
Instead of just saying “well, if you don’t like it, don’t use it and shut up” (this is how it reads), I think it would be better to try to address what many of use are considering a flaw in the design. Can we discuss instead what could be modified from the proposed design so that our concern handled in a better way?
@Goodwine
Let’s say it’s a good practice to wrap errors with useful context,
try()
would be considered a bad practice because it’s not adding any context. This means thattry()
is a feature nobody wants to use and become a feature that’s used so rarely that it may as well not have existed.
As noted in the proposal (and shown by example), try
doesn’t fundamentally prevent you from adding context. I’d say that the way it is proposed, adding context to errors is entirely orthogonal to it. This is addressed specifically in the FAQ of the proposal.
I recognize that try
won’t be useful if within a single function if there are a multitude of different contexts that you want to add to different errors from function calls. However, I also believe that something in the general vein of HandleErrorf
covers a large area of use because only adding function-wide context to errors is not unusual.
Instead of just saying “well, if you don’t like it, don’t use it and shut up” (this is how it reads), I think it would be better to try to address what many of use are considering a flaw in the design.
Another thing occurs to me: I’m seeing a lot of criticism based on the idea that having try
might encourage developers to handle errors carelessly. But in my opinion this is, if anything, more true of the current language; the error-handling boilerplate is annoying enough that it encourages one to swallow or ignore some errors to avoid it. For instance, I’ve written things like this a few times:
func exists(filename string) bool {
_, err := os.Stat(filename)
return err == nil
}
in order to be able to write if exists(...) { ... }
, even though this code silently ignores some possible errors. If I had try
, I probably would not bother to do that and just return (bool, error)
.
…
1) The most important concern appears to be that try
does not encourage good error handling style but instead promotes the “quick exit”. (@agnivade, @peterbourgon, @politician, @a8m, @eandre, @prologic, @kungfusheep, @cpuguy, and others have voiced their concern about this.)
…
1) It would be good to learn more about this concern. The current coding style using if
statements to test for errors is about as explicit as it can be. It’s very easy to add additional information to an error, on an individual basis (for each if
). Often it makes sense to handle all errors detected in a function in a uniform way, which can be done with a defer
- this is already possible now. It is the fact that we already have all the tools for good error handling in the language, and the problem of a handler construct not being orthogonal to defer
, that led us to leave away a new mechanism solely for augmenting errors.
It would be good to learn more about this concern. The current coding style using if statements to test for errors is about as explicit as it can be. It’s very easy to add additional information to an error, on an individual basis (for each if). Often it makes sense to handle all errors detected in a function in a uniform way, which can be done with a defer - this is already possible now. It is the fact that we already have all the tools for good error handling in the language, and the problem of a handler construct not being orthogonal to defer, that led us to leave away a new mechanism solely for augmenting errors.
@griesemer - IIUC, you are saying that for callsite-dependent error contexts, the current if statement is fine. Whereas, this new try
function is useful for the cases where handling multiple errors at a single place is useful.
I believe the concern was that, while simply doing a if err != nil { return err}
may be fine for some cases, it is usually recommended to decorate the error before returning. And this proposal seems to address the previous and does not do much for the latter. Which essentially means folks will be encouraged to use easy-return pattern.
@agnivade You are correct, this proposal does exactly nothing to help with error decoration (but to recommend the use of defer
). One reason is that language mechanisms for this already exist. As soon as error decoration is required, especially on an individual error basis, the additional amount of source text for the decoration code makes the if
less onerous in comparison. It’s the cases where no decoration is required, or where the decoration is always the same, where the boilerplate becomes a visible nuisance and then detracts from the important code.
Folks are already encouraged to use an easy-return pattern, try
or no try
, there’s just less to write. Come to think of it, the only way to encourage error decoration is to make it mandatory, because no matter what language support is available, decorating errors will require more work.
One way to sweeten the deal would be to only permit something like try
(or any analogous shortcutting notation) if an explicit (possibly empty) handler is provided somewhere (note that the original draft design didn’t have such a requirement, either).
I’m not sure we want to go so far. Let me restate that plenty of perfectly fine code, say internals of a library, does not need to decorate errors everywhere. It’s fine to just propagate errors up and decorate them just before they leave the API entry points, for instance. (In fact, decorating them everywhere will only lead to overdecorated errors that, with the real culprits hidden, make it harder to locate the important errors; very much like overly verbose logging can make it difficult to see what’s really going on).
Error Context
The most important semantic concern that’s been raised in this issue is whether try will encourage better or worse annotation of errors with context.
The Problem Overview from last August gives a sequence of example CopyFile implementations in the Problem and Goals sections. It is an explicit goal, both back then and today, that any solution make it more likely that users add appropriate context to errors. And we think that try can do that, or we wouldn’t have proposed it.
But before we get to try, it is worth making sure we’re all on the same page about appropriate error context. The canonical example is os.Open. Quoting the Go blog post “Error handling and Go”:
It is the error implementation’s responsibility to summarize the context. The error returned by os.Open formats as “open /etc/passwd: permission denied,” not just “permission denied.”
See also Effective Go’s section on Errors.
Note that this convention may differ from other languages you are familiar with, and it is also only inconsistently followed in Go code. An explicit goal of trying to streamline error handling is to make it easier for people to follow this convention and add appropriate context, and thereby to make it followed more consistently.
There is lots of code following the Go convention today, but there is also lots of code assuming the opposite convention. It’s too common to see code like:
f, err := os.Open(file)
if err != nil {
log.Fatalf("opening %s: %v", file, err)
}
which of course prints the same thing twice (many examples in this very discussion look like this). Part of this effort will have to be making sure everyone knows about and is following the convention.
In code following the Go error context convention, we expect that most functions will properly add the same context to each error return, so that one decoration applies in general. For example, in the CopyFile example, what needs to be added in each case is details about what was being copied. Other specific returns might add more context, but typically in addition rather than in replacement. If we’re wrong about this expectation, that would be good to know. Clear evidence from real code bases would help.
The Gophercon check/handle draft design would have used code like:
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)
...
}
This proposal has revised that, but the idea is the same:
func CopyFile(src, dst string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}()
r := try(os.Open(src))
defer r.Close()
w := try(os.Create(dst))
...
}
and we want to add an as-yet-unnamed helper for this common pattern:
func CopyFile(src, dst string) (err error) {
defer HelperToBeNamedLater(&err, "copy %s %s", src, dst)
r := try(os.Open(src))
defer r.Close()
w := try(os.Create(dst))
...
}
In short, the reasonability and success of this approach depends on these assumptions and logical steps:
If there is an assumption or logical step that you think is false, we want to know. And the best way to tell us is to point to evidence in actual code bases. Show us common patterns you have where try is inappropriate or makes things worse. Show us examples of things where try was more effective than you expected. Try to quantify how much of your code base falls on one side or the other. And so on. Data matters.
Thanks.
Thanks @rsc for the additional info on error context best practice. This point on best practice in particular has alluded me, but significantly improves try
s relationship to error context.
Therefore most functions only need to add function-level context describing the overall operation, not the specific sub-piece that failed (that sub-piece self-reported already).
So then the place where try
does not help is when we need to react to errors, not just contextualize them.
To adapt an example from Cleaner, more elegant, and wrong, here their example of a function that is subtly wrong in its error handling. I’ve adapted it to Go using try
and defer
-style error wrapping:
func AddNewGuy(name string) (guy Guy, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("adding guy %v: %v", name, err)
}
}()
guy = Guy{name: name}
guy.Team = ChooseRandomTeam()
try(guy.Team.Add(guy))
try(AddToLeague(guy))
return guy, nil
}
This function is incorrect because if guy.Team.Add(guy)
succeeds but AddToLeague(guy)
fails, the team will have an invalid Guy object that isn’t in a league. The correct code would look like this, where we roll back guy.Team.Add(guy)
and can no longer use try
:
func AddNewGuy(name string) (guy Guy, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("adding guy %v: %v", name, err)
}
}()
guy = Guy{name: name}
guy.Team = ChooseRandomTeam()
try(guy.Team.Add(guy))
if err := AddToLeague(guy); err != nil {
guy.Team.Remove(guy)
return Guy{}, err
}
return guy, nil
}
Or, if we want to avoid having to provide zero values for the non-error return values, we can replace return Guy{}, err
with try(err)
. Regardless, the defer
-ed function is still run and context is added, which is nice.
Again, this means that try
punts on reacting to errors, but not on adding context to them. That’s a distinction that has alluded me and perhaps others. This makes sense because the way a function adds context to an error is not of particular interest to a reader, but the way a function reacts to errors is important. We should be making the less interesting parts of our code less verbose, and that’s what try
does.
@velovix, re https://github.com/golang/go/issues/32437#issuecomment-503314834:
Again, this means that
try
punts on reacting to errors, but not on adding context to them. That’s a distinction that has alluded me and perhaps others. This makes sense because the way a function adds context to an error is not of particular interest to a reader, but the way a function reacts to errors is important. We should be making the less interesting parts of our code less verbose, and that’s whattry
does.
This is a really nice way to put it. Thanks.
https://github.com/golang/go/issues/32437#issuecomment-503297387 pretty much says if you’re wrapping errors in more than one way in a single function, you’re apparently doing it wrong. Meanwhile, I have a lot of code that looks like this:
if err := gen.Execute(tmp, s); err != nil {
return fmt.Errorf("template error: %v", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("cannot write temp file: %v", err)
}
closed = true
if err := os.Rename(tmp.Name(), *genOutput); err != nil {
return fmt.Errorf("cannot finalize file: %v", err)
}
removed = true
(closed
and removed
are used by defers to clean up, as appropriate)
I really don’t think all of these should just be given the same context describing the top-level mission of this function. I really don’t think the user should just see
processing path/to/dir: template: gen:42:17: executing "gen" at <.Broken>: can't evaluate field Broken in type main.state
when the template is screwed up, I think it’s the responsibility of my error handler for the template Execute call to add “executing template” or some such little extra bit. (That’s not the greatest bit of context, but I wanted to copy-paste real code instead of a made-up example.)
I don’t think the user should see
processing path/to/dir: rename /tmp/blahDs3x42aD commands.gen.go: No such file or directory
without some clue of why my program is trying to make that rename happen, what is the semantics, what is the intent. I believe adding that little bit of “cannot finalize file:” really helps.
If these examples don’t convince you enough, imagine this error output from a command-line app:
processing path/to/dir: open /some/path/here: No such file or directory
What does that mean? I want to add a reason why the app tried to create a file there (You didn’t even know it was a create, not just os.Open! It’s ENOENT because an intermediate path doesn’t exist.). This is not something that should be added to every error return from this function.
So, what am I missing. Am I “holding it wrong”? Am I supposed to push each of those things into a separate tiny function that all use a defer to wrap all of their errors?
@tv42 From the examples in your https://github.com/golang/go/issues/32437#issuecomment-503340426, assuming you’re not doing it “wrong”, it seems like using an if
statement is the way to handle these cases if they all require different responses. try
won’t help, and defer
will only make it harder (any other language change proposal in this thread which is trying to make this code simpler to write is so close to the if
statement that it’s not worth introducing new mechanism). See also the FAQ of the detailed proposal.
@griesemer Then all I can think of is that you and @rsc disagree. Or that I am, indeed, “doing it wrong”, and would like to have a conversation about that.
It is an explicit goal, both back then and today, that any solution make it more likely that users add appropriate context to errors. And we think that try can do that, or we wouldn’t have proposed it.
@tv42 @rsc post is about overall error handling structure of good code, which I agree with. If you have an existing piece of code that doesn’t fit this pattern exactly and you’re happy with the code, leave it alone.
@tv42, I agree with @griesemer. If you find that additional context is needed to smooth over a connection like the rename being a “finalize” step, there is nothing wrong with using if statements to add additional context. In many functions, however, there is little need for such additional context.
I found myself exactly in the situation described in https://github.com/golang/go/issues/32437#issuecomment-503297387
In some level i wrap errors individually, i will not change this with try
, it’s fine with if err!=nil
.
At other level i just return err
it’s a pain to add the same context for all return, then i will use try
and defer
.
I even already do this with a specif logger that i use in begin of function just in case of error. For me try
and decoration by function is already goish.
I appreciate the effort that went into this. I think it’s the most go-ey solution I’ve seen so far. But I think it introduces a bunch of work when debugging. Unwrapping try and adding an if block every time I debug and rewrapping it when I’m done is tedious. And I also have some cringe about the magical err variable that I need to consider. I’ve never been bothered by the explicit error checking so perhaps I’m the wrong person to ask. It always struck me as “ready to debug”.
I share the concern as @deanveloper and others that it might make debugging tougher. It’s true that we can choose not to use it, but the styles of third-party dependencies are not under our control.
If less repetitive if err := ... { return err }
is the primary point, I wonder if a “conditional return” would suffice, like https://github.com/golang/go/issues/27794 proposed.
return nil, err if f, err := os.Open(...)
return nil, err if _, err := os.Write(...)
I’m concerned that try
will supplant traditional error handling, and that that will make annotating error paths more difficult as a result.
Code that handles errors by logging messages and updating telemetry counters will be looked upon as defective or improper by both linters and developers expecting to try
everything.
a, b, err := doWork()
if err != nil {
updateCounters()
writeLogs()
return err
}
Go is an extremely social language with common idioms enforced by tooling (fmt, lint, etc). Please keep the social ramifications of this idea in mind - there will be a tendency to want to use it everywhere.
Thinking further about the conditional return briefly mentioned at https://github.com/golang/go/issues/32437#issuecomment-498947603. It seems
would be more compliant with how Go's exising `if` looks.
If we add a rule for the `return if` statement that
*when the last condition expression (like `err != nil`) is not present,
and the last variable of the declaration in the `return if ` statement is of the type `error`,
then the value of the last variable will be automatically compared with `nil` as the implicit condition.*
Then the `return if` statement can be abbreviated into:
`return if f, err := os.Open("my/file/path")`
Which is very close to the signal-noise ratio that the `try` provides.
If we change the `return if` to `try`, it becomes
```try f, err := os.Open("my/file/path")```
It again becomes similar to other proposed variations of the `try` in this thread, at least syntactically.
Personally, I still prefer `return if` over `try` in this case because it makes the exit points of a function very explicit. For instance, when debugging I often highlight the keyword `return` within the editor to identify all exit points of a large function.
Unfortunately, it doesn't seem to help enough with the inconvenience of inserting debug logging either.
Unless we also allow a `body` block for `return if`, like
Original:
return if f, err := os.Open("my/path")
When debugging:
The meaning of the body block of
return ifis obvious, I assume. It will be executed before
defer` and return.
That said, I don’t have complaints with the existing error-handling approach in Go.
I am more concerned about how the addition of the new error-handling would impact the present goodness of Go.…
4) Using try
will make it harder to debug code; for instance, it may be necessary to rewrite a try
expression back into an if
statement just so that debugging statements can be inserted (@deanveloper, @typeless, @networkimprov, others).
…
4) The debugging point is a valid concern. If there’s a need to add code between detecting an error and a return
, having to rewrite atry
expression into an if
statement could be annoying.
It would be good if the “try” proposal explicitly called out the consequences for tools such as cmd/cover that approximate test coverage stats using naive statement counting. I worry that the invisible error control flow might result in undercounting.
This probably has been covered before so I apologize for adding even more noise but just wanted to make a point about try builtin vs the try … else idea.
I think try builtin function can be a bit frustrating during development. We might occasionally want to add debug symbols or add more error specific context before returning. One would have to re-write a line like
user := try(getUser(userID))
to
user, err := getUser(userID)
if err != nil {
// inspect error here
return err
}
Adding a defer statement can help but it’s still not the best experience when a function throws multiple errors as it would trigger for every try() call.
Re-writing multiple nested try() calls in the same function would be even more annoying.
On the other hand, adding context or inspection code to
user := try getUser(userID)
would be as simple as adding a catch statement at end followed by the code
user := try getUser(userID) catch {
// inspect error here
}
Removing or temporarily disabling a handler would be as simple as breaking the line before catch and commenting it out.
Switching between try()
and if err != nil
feels a lot more annoying IMO.
This also applies to adding or removing error context. One can write try func()
while prototyping something very quickly and then add context to specific errors as needed as the program matures as opposed to try()
as a built-in where one would have to re-write the lines to add context or add extra inspection code during debugging.
I’m sure try() would be useful but as I imagine using it in my day to day work, I can’t help but imagine how try ... catch
would be so much more helpful and much less annoying when I’d need to add/remove extra code specific to some errors.
Also, I feel that adding try()
and then recommending to use if err != nil
to add context is very similar to having make()
vs new()
vs :=
vs var
. These features are useful in different scenarios but wouldn’t it be nice if we had less ways or even a single way to initialize variables? Of course no one is forcing anyone to use try and people can continue to use if err != nil but I feel this will split error handling in Go just like the multiple ways to assign new variables. I think whatever method is added to the language should also provide a way to easily add/remove error handlers instead of forcing people to rewrite entire lines to add/remove handlers. That doesn’t feel like a good outcome to me.
Sorry again for the noise but wanted to point it out in case someone wanted to write a separate detailed proposal for the try ... else
idea.
//cc @brynbellomy
Thanks, @owais, for bringing this up again - it’s a fair point (and the debugging issue has indeed been mentioned before). try
does leave the door open for extensions, such as a 2nd argument, which could be a handler function. But it is true that a try
function doesn’t make debugging easier - one may have to rewrite the code a bit more than a try
-catch
or try
- else
.
@owais
Adding a defer statement can help but it’s still not the best experience when a function throws multiple errors as it would trigger for every try() call.
You could always include a type switch in the deferred function which would handle (or not) different types of error in an appropriate way before returning.
Hi so just to add on to the whole issue with adding debug statements and such during development.
I think that the second parameter idea is fine for the try()
function, but another idea just to throw it out there is by adding an emit
clause to be a second part for try()
.
For instance, I believe when developing and such there could be a case when I want to call fmt
for this instant to print the error. So I could go from this:
func writeStuff(filename string) (io.ReadCloser, error) {
f := try(os.Open(filename))
try(fmt.Fprintf(f, "stuff\n"))
return f, nil
}
Can be re-written to something like this for debug statements or general handling or the error before returning.
func writeStuff(filename string) (io.ReadCloser, error) {
emit err {
fmt.Printf("something happened [%v]\n", err.Error())
return nil, err
}
f := try(os.Open(filename))
try(fmt.Fprintf(f, "stuff\n"))
return f, nil
}
So here I did end up putting a proposal for a new keyword emit
which could be a statement or a one liner for immediate returning like the initial try()
functionality:
emit return nil, err
What the emit would be is essentially just a clause where you can put any logic you wish in it if the try()
gets triggered by an error not equalling nil. Another ability with the emit
keyword is that you’re able to access the error right there if you add just after the keyword a variable name such as I did in the first example using it.
This proposal does create a little verbosity to the try()
function, but I think it’s at least a little more clear on what is happening with the error. This way you’re able to decorate the errors too without having it jammed all into one line and you can see how the errors are handles immediately when you’re reading the function.
@damienfamed75 Your proposedemit
statement looks essentially the same as the handle
statement of the draft design. The primary reason for abandoning the handle
declaration was its overlap with defer
. It’s not clear to me why one couldn’t just use a defer
to get the same effect that emit
achieves.
@griesemer about your concern about defer. I was going for that emit
or in the initial proposal handle
. The emit/handle
would be called if the err
is not nil. And will initiate at that moment instead of at the end of the function. The defer gets called at the end. emit/handle
WOULD end the function based on if err
is nil or not. That’s why defer wouldn’t work.
@damienfamed75 Thanks for your explanations. So the emit
will be called when try
finds an error, and it’s called with that error. That seems clear enough.
You’re also saying that the emit
would end the function if there’s an error, and not if the error was handled somehow. If you don’t end the function, where does the code continue? Presumably with returning from try
(otherwise I don’t understand the emit
that doesn’t end the function). Wouldn’t it be easier and clearer in that case to just use an if
instead of try
? Using an emit
or handle
would obscure control flow tremendously in those cases, especially because the emit
clause can be in a completely different part (presumably earlier) in the function. (On that note, can one have more than one emit
? If not, why not? What happens if there isn’t an emit
? Lots of the same questions that plagued the original check
/handle
draft design.)
Only if one wants to return from a function w/o much extra work besides error decoration, or with always the same work, does it make sense to use try
, and some sort of handler. And that handler mechanism, which runs before a function returns, exists already in defer
.
@griesemer I will try to address all your concerns one by one from your last response.
First you asked about if the handler doesn’t return or exit the function in some way then what would happen. Yes there can be instances where the emit
/handle
clause won’t return or exit a function, but rather pick up where it left off. For instance, in the case that we are trying to find a delimiter or something simple using a reader and we reach the EOF
we may not want to return an error when we hit that. So I built this quick example of what that could look like:
func findDelimiter(r io.Reader) ([]byte, error) {
emit err {
// if this doesn't return then continue from where we left off
// at the try function that was called last.
if err != io.EOF {
return nil, err
}
}
bufReader := bufio.NewReader(r)
token := try(bufReader.ReadSlice('|'))
return token, nil
}
Or even could be further simplified to this:
func findDelimiter(r io.Reader) ([]byte, error) {
emit err != io.EOF {
return nil, err
}
bufReader := bufio.NewReader(r)
token := try(bufReader.ReadSlice('|'))
return token, nil
}
Second concern was about disruption of control flow. And yes it would disrupt the flow, but to be fair most of the proposals are somewhat disrupting the flow to have one central error handling function and such. This is no different I believe.
Next, you asked about if we used emit
/handle
more than once in which I say that it’s redefined.
If you use emit
more than once it will overwrite the last one and so on. If you do not have any then the try
will have a default handler that just returns nil values and the error. That means that this example here:
func writeStuff(filename string) (io.ReadCloser, error) {
emit err {
return nil, err
}
f := try(os.Open(filename))
try(fmt.Fprintf(f, "stuff\n"))
return f, nil
}
Would do the same thing as this example:
func writeStuff(filename string) (io.ReadCloser, error) {
// when not defining a handler then try's default handler kicks in to
// return nil valued then error as usual.
f := try(os.Open(filename))
try(fmt.Fprintf(f, "stuff\n"))
return f, nil
}
Your last question was about declaring a handler function that is called in a defer
with I assume a reference to an error
. This design doesn’t work in the same way that this proposal works in the grounds that a defer
can’t immediately stop a function given a condition itself.
I believe I addressed everything in your response and I hope this clears up my proposal just a little bit more. If there are anymore concerns then let me know because I think this whole discussion with everybody is quite fun to ponder new ideas. Keep up the great work everyone!
…
In my experience, the most common form of error handling code is code that essentially adds a stack trace, sometimes with added context. I’ve found that stack trace to be very important for debugging, where I follow an error message through the code.
But, maybe other proposals will add stack traces to all errors? I’ve lost track.
In the example @adg gave, there are two potential failures but no context. If newScanner
and RunMigrations
don’t themselves provide messages that clue you into which one went wrong, then you’re left guessing.
…
Firstly, although this proposal doesn’t make it easy to add context-specific error text to an error, it does make it easy to add stack frame error-tracing information to an error: https://play.golang.org/p/YL1MoqR08E6
@rogpeppe comment if try
auto-adds the stack frame, not me, I’m ok with it discouraging adding context.
One situation where I find error checking via if
particularly awkward is when closing files (e.g. on NFS). I guess, currently we are meant to write the following, if error returns from .Close()
are possible?
r, err := os.Open(src)
if err != nil {
return err
}
defer func() {
// maybe check whether a previous error occured?
return r.Close()
}()
Could defer try(r.Close())
be a good way to have a manageable syntax for some way of dealing with such errors? At least, it would make sense to adjust the CopyFile()
example in the proposal in some way, to not ignore errors from r.Close()
and w.Close()
.
@seehuhn Your example won’t compile because the deferred function does not have a return type.
func doWork() (err error) {
r, err := os.Open(src)
if err != nil {
return err
}
defer func() {
err = r.Close() // overwrite the return value
}()
}
Will work like you expect. The key is the named return value.
I like the proposal but I think that the example of @seehuhn should be adressed as well :
defer try(w.Close())
would return the error from Close() only if the error was not already set. This pattern is used so often…
I was somewhat concerned about the readability of programs where try
appears inside other expressions. So I ran grep "return .*err$"
on the standard library and started reading through blocks at random. There are 7214 results, I only read a couple hundred.
The first thing of note is that where try
applies, it makes almost all of these blocks a little more readable.
The second thing is that very few of these, less than 1 in 10, would put try
inside another expression. The typical case is statements of the form x := try(...)
or ^try(...)$
.
Here are a few examples where try
would appear inside another expression:
text/template
func gt(arg1, arg2 reflect.Value) (bool, error) {
// > is the inverse of <=.
lessOrEqual, err := le(arg1, arg2)
if err != nil {
return false, err
}
return !lessOrEqual, nil
}
becomes:
func gt(arg1, arg2 reflect.Value) (bool, error) {
// > is the inverse of <=.
return !try(le(arg1, arg2)), nil
}
text/template
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
switch v.Kind() {
case reflect.Map:
index, err := prepareArg(index, v.Type().Key())
if err != nil {
return reflect.Value{}, err
}
if x := v.MapIndex(index); x.IsValid() {
v = x
} else {
v = reflect.Zero(v.Type().Elem())
}
...
}
}
becomes
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
switch v.Kind() {
case reflect.Map:
if x := v.MapIndex(try(prepareArg(index, v.Type().Key()))); x.IsValid() {
v = x
} else {
v = reflect.Zero(v.Type().Elem())
}
...
}
}
(this is the most questionable example I saw)
regexp/syntax:
regexp/syntax/parse.go
func Parse(s string, flags Flags) (*Regexp, error) {
...
if c, t, err = nextRune(t); err != nil {
return nil, err
}
p.literal(c)
...
}
becomes
func Parse(s string, flags Flags) (*Regexp, error) {
...
c, t = try(nextRune(t))
p.literal(c)
...
}
This is not an example of try inside another expression but I want to call it out because it improves readability. It’s much easier to see here that the values of c
and t
are living beyond the scope of the if statement.
net/http
net/http/request.go:readRequest
mimeHeader, err := tp.ReadMIMEHeader()
if err != nil {
return nil, err
}
req.Header = Header(mimeHeader)
becomes:
req.Header = Header(try(tp.ReadMIMEHeader())
database/sql
if driverCtx, ok := driveri.(driver.DriverContext); ok {
connector, err := driverCtx.OpenConnector(dataSourceName)
if err != nil {
return nil, err
}
return OpenDB(connector), nil
}
becomes
if driverCtx, ok := driveri.(driver.DriverContext); ok {
return OpenDB(try(driverCtx.OpenConnector(dataSourceName))), nil
}
database/sql
si, err := ctxDriverPrepare(ctx, dc.ci, query)
if err != nil {
return nil, err
}
ds := &driverStmt{Locker: dc, si: si}
becomes
ds := &driverStmt{
Locker: dc,
si: try(ctxDriverPrepare(ctx, dc.ci, query)),
}
net/http
if http2isconnectioncloserequest(req) && dialonmiss {
// it gets its own connection.
http2tracegetconn(req, addr)
const singleuse = true
cc, err := p.t.dialclientconn(addr, singleuse)
if err != nil {
return nil, err
}
return cc, nil
}
becomes
if http2isconnectioncloserequest(req) && dialonmiss {
// it gets its own connection.
http2tracegetconn(req, addr)
const singleuse = true
return try(p.t.dialclientconn(addr, singleuse))
}
net/http
func (f *http2Framer) endWrite() error {
...
n, err := f.w.Write(f.wbuf)
if err == nil && n != len(f.wbuf) {
err = io.ErrShortWrite
}
return err
}
becomes
func (f *http2Framer) endWrite() error {
...
if try(f.w.Write(f.wbuf) != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
(This one I really like.)
net/http
if f, err := fr.ReadFrame(); err != nil {
return nil, err
} else {
hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
}
becomes
hc = try(fr.ReadFrame()).(*http2ContinuationFrame)// guaranteed by checkFrameOrder
}
(Also nice.)
net:
if ctrlFn != nil {
c, err := newRawConn(fd)
if err != nil {
return err
}
if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
return err
}
}
becomes
if ctrlFn != nil {
try(ctrlFn(fd.ctrlNetwork(), laddr.String(), try(newRawConn(fd))))
}
maybe this is too much, and instead it should be:
if ctrlFn != nil {
c := try(newRawConn(fd))
try(ctrlFn(fd.ctrlNetwork(), laddr.String(), c))
}
Overall, I quite enjoy the effect of try
on the standard library code I read through.
One final point: Seeing try
applied to read code beyond the few examples in the proposal was enlightening. I think it is worth considering writing a tool to automatically convert code to use try
(where it doesn’t change the semantics of the program). It would be interesting to read a sample of the diffs is produces against popular packages on github to see if what I found in the standard library holds up. Such a program’s output could provide extra insight into the effect of the proposal.
@crawshaw thanks for doing this, it was great to see it in action. But seeing it in action made me take more seriously the arguments against inline error handling that I had until now been dismissing.
Since this was in such close proximity to @thepudds interesting suggestion of making try
a statement, I rewrote all of the examples using that syntax and found it much clearer than either the expression-try
or the status quo, without requiring too many extra lines:
func gt(arg1, arg2 reflect.Value) (bool, error) {
// > is the inverse of <=.
try lessOrEqual := le(arg1, arg2)
return !lessOrEqual, nil
}
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
switch v.Kind() {
case reflect.Map:
try index := prepareArg(index, v.Type().Key())
if x := v.MapIndex(index); x.IsValid() {
v = x
} else {
v = reflect.Zero(v.Type().Elem())
}
...
}
}
func Parse(s string, flags Flags) (*Regexp, error) {
...
try c, t = nextRune(t)
p.literal(c)
...
}
try mimeHeader := tp.ReadMIMEHeader()
req.Header = Header(mimeHeader)
if driverCtx, ok := driveri.(driver.DriverContext); ok {
try connector := driverCtx.OpenConnector(dataSourceName)
return OpenDB(connector), nil
}
This one would arguably be better with an expression-try
if there were multiple fields that had to be try
-ed, but I still prefer the balance of this trade off
try si := ctxDriverPrepare(ctx, dc.ci, query)
ds := &driverStmt{Locker: dc, si: si}
This is basically the worst-case for this and it looks fine:
if http2isconnectioncloserequest(req) && dialonmiss {
// it gets its own connection.
http2tracegetconn(req, addr)
const singleuse = true
try cc := p.t.dialclientconn(addr, singleuse)
return cc, nil
}
I debated with myself whether if try
would or should be legal, but I couldn’t come up with a reasonable explanation why it shouldn’t be and it works quite well here:
func (f *http2Framer) endWrite() error {
...
if try n := f.w.Write(f.wbuf); n != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
try f := fr.ReadFrame()
hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
if ctrlFn != nil {
try c := newRawConn(fd)
try ctrlFn(fd.ctrlNetwork(), laddr.String(), c)
}
Scanning through @crawshaw’s examples only makes me feel more sure that control flow will be often made cryptic enough to be even more careful about the design. Relating even a small amount of complexity becomes difficult to read and easy to botch. I’m glad to see options considered, but complicating control flow in such a guarded language seems exceptionally out of character.
func (f *http2Framer) endWrite() error {
...
if try(f.w.Write(f.wbuf) != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
Also, try
is not “trying” anything. It is a “protective relay”. If the base semantics of the proposal is off, I’m not surprised the resulting code is also problematic.
First, I applaud @crawshaw for taking the time to look at roughly 200 real examples and taking the time for his thoughtful write-up above.
Second, @jimmyfrasche, regarding your response here about the http2Framer
example:
I debated with myself whether
if try
would or should be legal, but I couldn’t come up with a reasonable explanation why it shouldn’t be and it works quite well here:func (f *http2Framer) endWrite() error { ... if try n := f.w.Write(f.wbuf); n != len(f.wbuf) { return io.ErrShortWrite } return nil }
At least under what I was suggesting above in https://github.com/golang/go/issues/32437#issuecomment-500213884, under that proposal variation I would suggest if try
is not allowed.
That http2Framer
example could instead be:
func (f *http2Framer) endWrite() error {
...
try n := f.w.Write(f.wbuf)
if n != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
That is one line longer, but hopefully still “light on the page”. Personally, I think that (arguably) reads more cleanly, but more importantly it is easier to see the try
.
@deanveloper wrote above in https://github.com/golang/go/issues/32437#issuecomment-498932961: > Returning from a function has seemed to have been a “sacred” thing to do
That specific http2Framer
example ends up being not as short as it possibly could be. However, it holds returning from a function more “sacred” if the try
must be the first thing on a line.
@crawshaw mentioned: > The second thing is that very few of these, less than 1 in 10, would put try inside another expression. The typical case is statements of the form x := try(…) or ^try(…)$.
Maybe it is OK to only partially help those 1 in 10 examples with a more restricted form of try
, especially if the typical case from those examples ends up with the same line count even if try
is required to be the first thing on a line?
@jimmyfrasche
@crawshaw thanks for doing this, it was great to see it in action. But seeing it in action made me take more seriously the arguments against inline error handling that I had until now been dismissing.
Since this was in such close proximity to @thepudds interesting suggestion of making
try
a statement, I rewrote all of the examples using that syntax and found it much clearer than either the expression-try
or the status quo, without requiring too many extra lines:func gt(arg1, arg2 reflect.Value) (bool, error) { // > is the inverse of <=. try lessOrEqual := le(arg1, arg2) return !lessOrEqual, nil }
Your first example illustrates well why I strongly prefer the expression-try
. In your version, I have to put the result of the call to le
in a variable, but that variable has no semantic meaning that the term le
doesn’t already imply. So there’s no name I can give it that isn’t either meaningless (like x
) or redundant (like lessOrEqual
). With expression-try
, no intermediate variable is needed, so this problem doesn’t even arise.
I’d rather not have to expend mental effort inventing names for things that are better left anonymous.
…
Re: @jimmyfrasche’s suggestion to allow try
within compound if
statements, that’s exactly the sort of thing I think many here are trying to avoid, for a few reasons:
- it conflates two very different control flow mechanisms into a single line
- the try
expression is actually evaluated first, and can cause the function to return, yet it appears after the if
- they return with totally different errors, one of which we don’t actually see in the code, and one which we do
- it makes it less obvious that the try
is actually unhandled, because the block looks a lot like a handler block (even though it’s handling a totally different problem)
One could approach this situation from a slightly different angle that favors pushing people to handle try
s. How about allowing the try
/else
syntax to contain subsequent conditionals (which is a common pattern with many I/O functions that return both an err
and an n
, either of which might indicate a problem):
func (f *http2Framer) endWrite() error {
// ...
try n := f.w.Write(f.wbuf) else err {
return errors.Wrap(err, "error writing:")
} else if n != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
In the case where you don’t handle the error returned by .Write
, you’d still have a clear annotation that .Write
might error (as pointed out by @thepudds):
func (f *http2Framer) endWrite() error {
// ...
try n := f.w.Write(f.wbuf)
if n != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
@davecheney @daved @crawshaw
I’d tend to agree with the Daves on this one: in @crawshaw’s examples, there are lots of try
statements embedded deep in lines that have a lot of other stuff going on. Really hard to spot exit points. Further, the try
parens seem to clutter things up pretty badly in some of the examples.
Seeing a bunch of stdlib code transformed like this is very useful, so I’ve taken the same examples but rewritten them per the alternate proposal, which is more restrictive:
- try
as a keyword
- only one try
per line
- try
must be at the beginning of a line
Hopefully this will help us compare. Personally, I find that these examples look a lot more concise than their originals, but without obscuring control flow. try
remains very visible anywhere it’s used.
text/template
func gt(arg1, arg2 reflect.Value) (bool, error) {
// > is the inverse of <=.
lessOrEqual, err := le(arg1, arg2)
if err != nil {
return false, err
}
return !lessOrEqual, nil
}
becomes:
func gt(arg1, arg2 reflect.Value) (bool, error) {
// > is the inverse of <=.
try lessOrEqual := le(arg1, arg2)
return !lessOrEqual, nil
}
text/template
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
switch v.Kind() {
case reflect.Map:
index, err := prepareArg(index, v.Type().Key())
if err != nil {
return reflect.Value{}, err
}
if x := v.MapIndex(index); x.IsValid() {
v = x
} else {
v = reflect.Zero(v.Type().Elem())
}
...
}
}
becomes
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
switch v.Kind() {
case reflect.Map:
try index := prepareArg(index, v.Type().Key())
if x := v.MapIndex(index); x.IsValid() {
v = x
} else {
v = reflect.Zero(v.Type().Elem())
}
...
}
}
regexp/syntax:
regexp/syntax/parse.go
func Parse(s string, flags Flags) (*Regexp, error) {
...
if c, t, err = nextRune(t); err != nil {
return nil, err
}
p.literal(c)
...
}
becomes
func Parse(s string, flags Flags) (*Regexp, error) {
...
try c, t = nextRune(t)
p.literal(c)
...
}
net/http
net/http/request.go:readRequest
mimeHeader, err := tp.ReadMIMEHeader()
if err != nil {
return nil, err
}
req.Header = Header(mimeHeader)
becomes:
try mimeHeader := tp.ReadMIMEHeader()
req.Header = Header(mimeHeader)
database/sql
if driverCtx, ok := driveri.(driver.DriverContext); ok {
connector, err := driverCtx.OpenConnector(dataSourceName)
if err != nil {
return nil, err
}
return OpenDB(connector), nil
}
becomes
if driverCtx, ok := driveri.(driver.DriverContext); ok {
try connector := driverCtx.OpenConnector(dataSourceName)
return OpenDB(connector), nil
}
database/sql
si, err := ctxDriverPrepare(ctx, dc.ci, query)
if err != nil {
return nil, err
}
ds := &driverStmt{Locker: dc, si: si}
becomes
try si := ctxDriverPrepare(ctx, dc.ci, query)
ds := &driverStmt{Locker: dc, si: si}
net/http
if http2isconnectioncloserequest(req) && dialonmiss {
// it gets its own connection.
http2tracegetconn(req, addr)
const singleuse = true
cc, err := p.t.dialclientconn(addr, singleuse)
if err != nil {
return nil, err
}
return cc, nil
}
becomes
if http2isconnectioncloserequest(req) && dialonmiss {
// it gets its own connection.
http2tracegetconn(req, addr)
const singleuse = true
try cc := p.t.dialclientconn(addr, singleuse)
return cc, nil
}
net/http
This one doesn’t actually save us any lines, but I find it much clearer because if err == nil
is a relatively uncommon construction.
func (f *http2Framer) endWrite() error {
...
n, err := f.w.Write(f.wbuf)
if err == nil && n != len(f.wbuf) {
err = io.ErrShortWrite
}
return err
}
becomes
func (f *http2Framer) endWrite() error {
...
try n := f.w.Write(f.wbuf)
if n != len(f.wbuf) {
return io.ErrShortWrite
}
return nil
}
net/http
if f, err := fr.ReadFrame(); err != nil {
return nil, err
} else {
hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
}
becomes
try f := fr.ReadFrame()
hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
}
net:
if ctrlFn != nil {
c, err := newRawConn(fd)
if err != nil {
return err
}
if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
return err
}
}
becomes
if ctrlFn != nil {
try c := newRawConn(fd)
try ctrlFn(fd.ctrlNetwork(), laddr.String(), c)
}
I also commend cranshaw for his work looking through the standard library, but I came to a very different conclusion… I think it makes nearly all those code snippets harder to read and more prone to misunderstanding.
req.Header = Header(try(tp.ReadMIMEHeader())
I will very often miss that this can error out. A quick read gets me “ok, set the header to Header of ReadMimeHeader of the thing”.
if driverCtx, ok := driveri.(driver.DriverContext); ok {
return OpenDB(try(driverCtx.OpenConnector(dataSourceName))), nil
}
This one, my eyes just cross trying to parse that OpenDB line. There’s so much density there… This shows the major problem that all nested function calls have, in that you have to read from the inside out, and you have to parse it in your head in order to figure out where the innermost part is.
Also note that this can return from two different places in the same line.. .you’re gonna be debugging, and it’s going to say there was an error returned from this line, and the first thing everyone is going to do is try to figure out why OpenDB is failing with this weird error, when it’s actually OpenConnector failing (or vice versa).
ds := &driverStmt{
Locker: dc,
si: try(ctxDriverPrepare(ctx, dc.ci, query)),
}
This is a place where the code can fail where previously it would be impossible. Without try
, struct literal construction cannot fail. My eyes will skim over it like “ok, constructing a driverStmt … moving on..” and it’ll be so easy to miss that actually, this can cause your function to error out. The only way that would have been possible before is if ctxDriverPrepare panicked… and we all know that’s a case that 1.) should basically never happen and 2.) if it does, it means something is drastically wrong.
Making try a keyword and a statement fixes a lot of my issues with it. I know that’s not backwards compatible, but I don’t think using a worse version of it is the solution to the backwards compatibility issue.
Like others, I would like to thank @crawshaw for the examples.
When reading those examples, I encourage people to try to adopt a mindset in which you don’t worry about the flow of control due to the try
function. I believe, perhaps incorrectly, that that flow of control will quickly become second nature to people who know the language. In the normal case, I believe that people will simply stop worrying about what happens in the error case. Try reading those examples while glazing over try
just as you already glaze over if err != nil { return err }
.
@ianlancetaylor
Try reading those examples while glazing over try just as you already glaze over if err != nil { return err }.
I don’t think that’s possible/equatable. Missing that a try exists in a crowded line, or what it exactly wraps, or that there are multiple instances in one line… These are not the same as easily/quickly marking a return point and not worrying about the specifics therein.
@ianlancetaylor
When I see a stop sign, I recognize it by shape and color more than by reading the word printed on it and pondering its deeper implications.
My eyes may glaze over if err != nil { return err }
but at the same time it still registers—clearly and instantly.
What I like about the try
-statement variant is that it reduces the boilerplate but in a way that is both easy to glaze over but hard to miss.
It may mean an extra line here or there but that’s still fewer lines than the status quo.
…
It’s tied to type error
and the last return value. If we need to inspect other return values/types for exceptional state, we’re back to: if errno := f(); errno != 0 { ... }
It doesn’t offer multiple pathways. Code that calls storage or networking APIs handles such errors differently than those due to incorrect input or unexpected internal state. My code does one of these far more often than return err
:
The proposal mentions changing package testing to allow tests and benchmarks to return an error. Though it wouldn’t be “a modest library change”, we could consider accepting func main() error
as well. It’d make writing little scripts much nicer. The semantics would be equivalent to:
func main() {
if err := newmain(); err != nil {
println(err.Error())
os.Exit(1)
}
}
@josharian Regarding main
returning an error
: It seems to me that your little helper function is all that’s needed to get the same effect. I’m not sure changing the signature of main
is justified.
@josharian
Though it wouldn’t be “a modest library change”, we could consider accepting func main() error as well.
The issue with that is that not all platforms have clear semantics on what that means. Your rewrite works well in “traditional” Go programs running on a full operating system - but as soon as you write microcontroller-firmware or even just WebAssembly, it’s not super clear what os.Exit(1)
would mean. Currently, os.Exit
is a library-call, so Go implementations are free just not to provide it. The shape of main
is a language concern though.
I just want to point out that you could not use this in main and it might be confusing to new users or when teaching. Obviously this applies to any function that doesn’t return an error but I think main is special since it appears in many examples..
func main() {
f := try(os.Open("foo.txt"))
defer f.Close()
}
I’m not sure making try panic in main would be acceptable either.
Additionally it would not be particularly useful in tests (func TestFoo(t* testing.T)
) which is unfortunate :(
…
try
doesn’t play well with the testing package for functions which don’t return an error value. My own preferred solution to this would be to have a second built-in function (perhaps ptry
or must
) which always panicked rather than returned on encountering a non-nil error and which could therefore be used with the aforementioned functions (including main
). Although this idea has been rejected in the present iteration of the proposal, I formed the impression it was a ‘close call’ and it may therefore be eligible for reconsideration.…
2) try
and testing: This can be addressed and made to work. See the detailed doc.
If the overarching function’s final return type is not of type error, can we panic?
It will make the builtin more versatile (such as satisfy my concern in #32219)
@pjebs This has been considered and decided against. Please read the detailed design doc (which explicitly refers to your issue on this subject).
The design says you explored using panic
instead of returning with the error.
I am highlighting a subtle difference:
Do exactly what your current proposal states, except remove restriction that overarching function must have a final return type of type error
.
If it doesn’t have a final return type of error
=> panic
If using try for package level variable declarations => panic (removes need for MustXXX( )
convention)
For unit tests, a modest language change.
…
@pjebs, that semantics - panic if there’s no error result in the current function - is exactly what the design doc is discussing in https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md#discussion.
Furthermore, in an attempt to make try useful not just inside functions with an error result, the semantics of try depended on the context: If try were used at the package-level, or if it were called inside a function without an error result, try would panic upon encountering an error. (As an aside, because of that property the built-in was called must rather than try in that proposal.) Having try (or must) behave in this context-sensitive way seemed natural and also quite useful: It would allow the elimination of many user-defined must helper functions currently used in package-level variable initialization expressions. It would also open the possibility of using try in unit tests via the testing package.
Yet, the context-sensitivity of try was considered fraught: For instance, the behavior of a function containing try calls could change silently (from possibly panicking to not panicking, and vice versa) if an error result was added or removed from the signature. This seemed too dangerous a property. The obvious solution would have been to split the functionality of try into two separate functions, must and try (very similar to what is suggested by issue #31442). But that would have required two new built-in functions, with only try directly connected to the immediate need for better error handling support.
@pjebs That is exactly what we considered in a prior proposal (see detailed doc, section on Design iterations, 4th paragraph):
Furthermore, in an attempt to make try useful not just inside functions with an error result, the semantics of try depended on the context: If try were used at the package-level, or if it were called inside a function without an error result, try would panic upon encountering an error. (As an aside, because of that property the built-in was called must rather than try in that proposal.)
The (Go Team internal) consensus was that it would be confusing for try
to depend on context and act so differently. For instance, adding an error result to a function (or remove it) could silently change the behavior of the function from panicking to not panicking (or vice versa).
@griesemer sorry I read a different section which discussed panic and didn’t see the discussion of the dangers.
…
return err
:
@gopherbot add Go2, LanguageChange
…
Using defer
, it appears the only control developers would be afforded is to branch to (the most recent?) defer
. But in my experience with any methods beyond a trivial func
it is usually more complicated than that.
Often I have found it to be helpful to share aspect of error handling within a func
— or even across a package
— but then also have more specific handling shared across one or more other packages.
For example, I may call five (5) func
calls that return an error()
from within another func
; let’s label them A()
, B()
, C()
, D()
, and E()
. I may need C()
to have its own error handling, A()
, B()
, D()
, and E()
to share some error handling, and B()
and E()
to have specific handling.
But I do not believe it would be possible to do that with this proposal. At least not easily.
Ironically, however, Go already has language features that allow a high level of flexibility that does not need to be limited to a small set of use-cases; func
s and closures. So my rhetorical question is:
“Why can’t we just add slight enhancements to the existing language to address these use-cases and not need to add new builtin functions or accept confusing semantics?”
It is a rhetorical question because I plan to submit a proposal as an alternative, one that I conceived of during the study of this proposal and while considering all its drawbacks.
But I digress, that will come later and this comment is about why the current proposal needs to be reconsidered.
break
This may feel like it comes out of left field as most people use early returns for error handling, but I have found it is be preferable to use break
for error handling wrapping most or all of a func prior to return
.
…
More importantly, my main gripe about handle
/check
was that it didn’t allow wrapping individual checks in different ways – and now this try()
proposal has the same flaw, while invoking tricky rarely-used newbie-confusing features of defers and named returns. And with handle
at least we had the option of using scopes to define handle blocks, with defer
even that isn’t possible.
As far as I’m concerned, this proposal loses to the earlier handle
/check
proposal in every single regard.
…
go try(f)
or defer try(f)
were doing and that it’s best therefore to just prohibit them altogether.…
3) This is explicitly addressed in the detailed doc.
Wouldn’t defer try(f())
be equivalent to
defer func() error {
if err:= f(); err != nil { return err }
return nil
}()
This (the if version) is currently not disallowed, right? Seems to me you should not make an exception here – may be generate a warning? And it is not clear if the defer code above is necessarily wrong. What if close(file)
fails in a defer
statement? Should we report that error or not?
I read the rationale which seems to talk about defer try(f)
not defer try(f())
. May be a typo?
A similar argument can be made for go try(f())
, which translates to
go func() error {
if err:= f(); err != nil { return err }
return nil
}()
Here try
doesn’t do anything useful but is harmless.
@bakul because arguments are evaluated immediately, it is actually roughly equivalent to:
<result list> := f()
defer try(<result list>)
This may be unexpected behavior to some as the f()
is not defered for later, it is executed right away. Same thing applies to go try(f())
.
@bakul The doc mentions defer try(f)
(rather than defer try(f())
because try
in general applies to any expression, not just a function call (you can say try(err)
for instance, if err
is of type error
). So not a typo, but perhaps confusing at first. f
simply stands for an expression, which usually happens to be a function call.
@deanveloper, @griesemer Never mind :-) Thanks.
In the detailed design document I noticed that in an earlier iteration it was suggested to pass an error handler to the try builtin function. Like this:
handler := func(err error) error {
return fmt.Errorf("foo failed: %v", err) // wrap error
}
f := try(os.Open(filename), handler)
or even better, like this:
f := try(os.Open(filename), func(err error) error {
return fmt.Errorf("foo failed: %v", err) // wrap error
})
Although, as the document states, that this raises several questions, I think this proposal would be far more more desirable and useful if it had kept this possibility to optionally specify such an error handler function or closure.
maybe we can add a variant with optional augmenting function something like tryf
with this semantics:
func tryf(t1 T1, t1 T2, … tn Tn, te error, fn func(error) error) (T1, T2, … Tn)
translates this
x1, x2, … xn = tryf(f(), func(err error) { return fmt.Errorf("foobar: %q", err) })
into this
t1, … tn, te := f()
if te != nil {
if fn != nil {
te = fn(te)
}
err = te
return
}
since this is an explicit choice (instead of using try
) we can find reasonable answers the questions in the earlier version of this design. for example if augmenting function is nil don’t do anything and just return the original error.
I agree with the concerns regarding adding context to errors. I see it as one of the best practices that keeps error messages much friendly (and clear) and makes debug process easier.
The first thing I thought about was to replace the fmt.HandleErrorf
with a tryf
function, that prefixs the error with additional context.
func tryf(t1 T1, t1 T2, … tn Tn, te error, ts string) (T1, T2, … Tn)
For example (from a real code I have):
func (c *Config) Build() error {
pkgPath, err := c.load()
if err != nil {
return nil, errors.WithMessage(err, "load config dir")
}
b := bytes.NewBuffer(nil)
if err = templates.ExecuteTemplate(b, "main", c); err != nil {
return nil, errors.WithMessage(err, "execute main template")
}
buf, err := format.Source(b.Bytes())
if err != nil {
return nil, errors.WithMessage(err, "format main template")
}
target := fmt.Sprintf("%s.go", filename(pkgPath))
if err := ioutil.WriteFile(target, buf, 0644); err != nil {
return nil, errors.WithMessagef(err, "write file %s", target)
}
// ...
}
Can be changed to something like:
func (c *Config) Build() error {
pkgPath := tryf(c.load(), "load config dir")
b := bytes.NewBuffer(nil)
tryf(emplates.ExecuteTemplate(b, "main", c), "execute main template")
buf := tryf(format.Source(b.Bytes()), "format main template")
target := fmt.Sprintf("%s.go", filename(pkgPath))
tryf(ioutil.WriteFile(target, buf, 0644), fmt.Sprintf("write file %s", target))
// ...
}
Or, if I take @agnivade’s example:
func (p *pgStore) DoWork() (err error) {
tx := tryf(p.handle.Begin(), "begin transaction")
defer func() {
if err != nil {
tx.Rollback()
}
}()
var res int64
tryf(tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res), "insert table")
_, = tryf(tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res), "insert table2")
return tryf(tx.Commit(), "commit transaction")
}
However, @josharian raised a good point that makes me hesitate on this solution: > As written, though, it moves fmt-style formatting from a package into the language itself, which opens up a can of worms.
…
Another solution would be to repurpose the (discarded) idea of having an optional second argument to try
for defining/whitelisting the error kind(s) that may be returned from that site. This is a bit troublesome because we have two different ways of defining an “error kind”, either by value (io.EOF
etc) or by type (*os.PathError
, *exec.ExitError
). It’s easy to specify error kinds that are values as arguments to a function, but harder to specify types. Not sure how to handle that, but throwing the idea out there.
Since error context seems to be a recurring theme…
Hypothesis: most Go functions return (T, error)
as opposed to (T1, T2, T3, error)
What if, instead of defining try
as try(T1, T2, T3, error) (T1, T2, T3)
we defined it as
try(func (args) (T1, T2, T3, error))(T1, T2, T3)
? (this is an approximation)
which is to say that the syntactic structure of a try
call is always a first argument that is an expression returning multiple values, the last of which is an error.
Then, much like make
, this opens the door to a 2-argument form of the call, where the second argument is the context of the try (e.g. a fixed string, a string with a %v
, a function that takes an error argument and returns another error etc.)
This still allows chaining for the (T, error)
case but you can no longer chain multiple returns which IMO is typically not required.
I would like to bring up once again the idea of a handler as a second argument to try
, but with the addition that the handler argument be required, but nil-able. This makes handling the error the default, instead of the exception. In cases where you really do want to pass the error up unchanged, simply provide a nil value to the handler and try
will behave just like in the original proposal, but the nil argument will act as a visual cue that the error is not being handled. It will be easier to catch during code review.
file := try(os.Open("my_file.txt"), nil)
What should happen if the handler is provided but is nil? Should try panic or treat it as an absent error handler?
As mentioned above, try
will behave in accordance with the original proposal. There would be no such thing as an absent error handler, only a nil one.
What if the handler is invoked with a non-nil error and then returns a nil result? Does this mean the error is “cancelled”? Or should the enclosing function return with a nil error?
I believe that the enclosing function would return with a nil error. It would potentially be very confusing if try
could sometimes continue execution even after it received a non-nil error value. This would allow for handlers to “take care” of the error in some circumstances. This behavior could be useful in a “get or create” style function, for example.
func getOrCreateObject(obj *object) error {
defaultObjectHandler := func(err error) error {
if err == ObjectDoesNotExistErr {
*obj = object{}
return nil
}
return fmt.Errorf("getting or creating object: %v", err)
}
*obj = try(db.getObject(), defaultObjectHandler)
}
It was also not clear if permitting an optional error handler would lead programmers to ignore proper error handling altogether. It would also be easy to do proper error handling everywhere but miss a single occurrence of a try. And so forth.
I believe that both of these concerns are alleviated by making the handler a required, nil-able argument. It requires programmers to make a conscious, explicit decision that they will not handle their error.
As a bonus, I think that requiring the error handler also discourages deeply nested try
s because they are less brief. Some might see this as a downside, but I think it’s a benefit.
@velovix I love the idea, but why does error-handler have to be required? Can’t it be nil
by default? Why do we need a “visual clue”?
@griesemer What if @velovix idea was adopted but with builtin
containing a predefined function that converts err to panic AND We remove requirement that over-arching function have an error return value?
The idea is, if the overarching function does not return error, using try
without error-handler is a compile-time error.
The error-handler can also be used to wrap the soon-to-be-returned error using various libraries etc at the location of error, instead of a defer
at the top that modifies a named returned error.
@pjebs
why does error-handler have to be required? Can’t it be nil by default? Why do we need a “visual clue”?
This is to address the concerns that
try
proposal as it is now could discourage people from providing context to their errors because doing so isn’t quite so straightforward.Having a handler in the first place makes providing context easier, and having the handler be a required argument sends a message: The common, recommended case is to handle or contextualize the error in some way, not simply pass it up the stack. It’s in line with the general recommendation from the Go community.
It was also not clear if permitting an optional error handler would lead programmers to ignore proper error handling altogether. It would also be easy to do proper error handling everywhere but miss a single occurrence of a try. And so forth.
Having to pass an explicit nil
makes it more difficult to forget to handle an error properly. You have to explicitly decide to not handle the error instead of doing so implicitly by leaving out an argument.
@velovix We quite liked the idea of a try
with an explicit handler function as 2nd argument. But there were too many questions that didn’t have obvious answers, as the design doc states. You have answered some of them in a way that seems reasonable to you. It’s quite likely (and that was our experience inside the Go Team), that somebody else thinks the correct answer is quite a different one. For instance, you are stating that the handler argument should always be provided, but that it can be nil
, to make it explicit we don’t care about handling the error. Now what happens if one provides a function value (not a nil
literal), and that function value (stored in a variable) happens to be nil? By analogy with the explicit nil
value, no handling is required. But others might argue that this is a bug in the code. Or, alternatively one could allow nil-valued handler arguments, but then a function might inconsistently handler errors in some cases and not in others, and it’s not necessarily obvious from the code which one do, because it appears as if a handler is always present. Another argument was that it’s better to have a top-level declaration of an error handler because that makes it very clear that the function does handle errors. Hence the defer
. There’s probably more.
…
Furthermore, I also like @velovix’s suggestion, and while I appreciate that this raises a few questions as described in the spec, I think these can be easily answered in a reasonable way, as @velovix already did.
For example:
What happens if one provides a function value (not a nil literal), and that function value (stored in a variable) happens to be nil? => Do not handle the error, period. This is useful in case the error handling depends on context and the handler variable is set depending on whether or not error handling is required. It’s not a bug, rather, it’s a feature. :)
Another argument was that it’s better to have a top-level declaration of an error handler because that makes it very clear that the function does handle errors. => So define the error handler at the top of the function as a named closure function and use that, so it’s also very clear that the error should be handled. This is not a serious issue, more of a style requirement.
What other concerns were there? I am pretty sure they can be all be answered similarly in a reasonable way.
Finally, as you say, “one way to sweeten the deal would be to only permit something like try (or any analogous shortcutting notation) if an explicit (possibly empty) handler is provided somewhere”. I think that Iif we are to proceed with this proposal, we should actually take it “this far”, to encourage proper, “explicit is better than implicit” error handling.
@griesemer
Now what happens if one provides a function value (not a nil literal), and that function value (stored in a variable) happens to be nil? By analogy with the explicit nil value, no handling is required. But others might argue that this is a bug in the code.
In theory this does seem like a potential gotcha, though I’m having a hard time conceptualizing a reasonable situation where a handler would end up being nil by accident. I imagine that handlers would most commonly either come from a utility function defined elsewhere, or as a closure defined in the function itself. Neither of these are likely to become nil unexpectedly. You could theoretically have a scenario where handler functions are being passed around as arguments to other functions, but to my eyes it seems rather far-fetched. Perhaps there’s a pattern like this that I’m not aware of.
Another argument was that it’s better to have a top-level declaration of an error handler because that makes it very clear that the function does handle errors. Hence the
defer
.
As @beoran mentioned, defining the handler as a closure near the top of the function would look very similar in style, and that’s how I personally expect people would be using handlers most commonly. While I do appreciate the clarity won by the fact that all functions that handle errors will be using defer
, it may become less clear when a function needs to pivot in its error handling strategy halfway down the function. Then, there will be two defer
s to look at and the reader will have to reason about how they will interact with each other. This is a situation where I believe a handler argument would be both more clear and ergonomic, and I do think that this will be a relatively common scenario.
…
Like make
, can we allow try
to take a variable number of parameters
- try(f):
as above.
a return error value is mandatory (as the last return parameter).
MOST COMMON USAGE MODEL
- try(f, doPanic bool):
as above, but if doPanic, then panic(err) instead of returning.
In this mode, a return error value is not necessary.
- try(f, fn):
as above, but call fn(err) before returning.
In this mode, a return error value is not necessary.
This way, it is one builtin that can handle all use-cases, while still being explicit. Its advantages: - always explicit - no need to infer whether to panic or set error and return - supports context-specific handler (but no handler chain) - supports use-cases where there is no error return variable - supports must(…) semantics
@ugorji Thanks for your positive feedback.
try
could be extended to take an additional argument. Our preference would be to take only a function with signature func (error) error
. If you want to panic, it’s easy to provide a one-line helper function:
func doPanic(err error) error { panic(err) }
Better to keep the design of try
simple.
@ugorji The try(f, bool)
variant you propose sounds like the must
from #32219.
@ugorji The
try(f, bool)
variant you propose sounds like themust
from #32219.
Yes, it is. I just felt like they all 3 cases could be handled with a singular builtin function, and satisfy all use-cases elegantly.
@dpinela , @ugorji Please read also the design doc on the subject of must
vs try
. It’s better to keep try
as simple as possible. must
is a common “pattern” in initialization expressions, but there’s no urgent need to “fix” that.
@ugorji
I think the boolean on try(f, bool)
would make it hard to read and easy to miss. I like your proposal but for the panic case I think that that could be left out for users to write that inside the handler from your third bullet, e.g. try(f(), func(err error) { panic('at the disco'); })
, this makes it more explicit for users than a hidden try(f(), true)
that is easy to overlook, and I don’t think the builtin functions should encourage panics.
@ugorji I think the boolean on
try(f, bool)
would make it hard to read and easy to miss. I like your proposal but for the panic case I think that that could be left out for users to write that inside the handler from your third bullet, e.g.try(f(), func(err error) { panic('at the disco'); })
, this makes it more explicit for users than a hiddentry(f(), true)
that is easy to overlook, and I don’t think the builtin functions should encourage panics.
On further thought, I tend to agree with your position and your reasoning, and it still looks elegant as a one-liner.
@beoran Regarding your comment that we should wait for generics. Generics won’t help here - please read the FAQ.
Regarding your suggestions on @velovix ’s 2-argument try
’s default behavior: As I said before, your idea of what is the obviously reasonable choice is somebody else’s nightmare.
May I suggest that we continue this discussion once a wide consensus evolves that try
with an explicit error handler is a better idea than the current minimal try
. At that point it makes sense to discuss the fine points of such a design.
(I do like having a handler, for that matter. It’s one of our earlier proposals. And if we adopt try
as is, we still can move towards a try
with a handler in a forward-compatible way - at least if the handler is optional. But let’s take one step at a time.)
…
6) Some have picked up on the idea of an optional error handler (@beoran) or format string provided to try
(@unexge, @a8m, @eandre, @gotwarlost) to encourage good error handling.
…
6) Optional handler argument to try
: The detailed document discusses this as well. See the section on Design iterations.
(I do like having a handler, for that matter. It’s one of our earlier proposals. And if we adopt try as is, we still can move towards a try with a handler in a forward-compatible way - at least if the handler is optional. But let’s take one step at a time.)
Sure, perhaps a multi-step approach is the way to go. If we add an optional handler argument in the future, tooling could be created to warn the writer of an unhandled try
in the same spirit as the errcheck
tool. Regardless, I appreciate your feedback!
As per https://github.com/golang/go/issues/32437#issuecomment-499320588:
func doPanic(err error) error { panic(err) }
I anticipate this function would be quite common. Could this be predefined in “builtin” (or somewhere else in a standard package eg errors
)?
@pjebs, I’ve written the equivalent function dozens of times. I usually call it “orDie” or “check”. It’s so simple, there’s no real need to make it part of the standard library. Plus different people may want logging or whatever before termination.
@carlmjohnson Yes it is simple but…
I’ve written the equivalent function dozens of times.
The advantages of a predeclared function is:
Since my previous post in support of the proposal, I’ve seen two ideas posted by @jagv (parameterless try
returns *error
) and by @josharian (labelled error handlers) which I believe in a slightly modified form would enhance the proposal considerably.
Putting theses ideas together with a further one I’ve had myself, we’d have four versions of try
:
#1 would simply return a pointer to the error return parameter (ERP) or nil if there wasn’t one (#4 only). This would provide an alternative to a named ERP without the need to add a further buult-in.
#2 would work exactly as currently envisaged. A non-nil error would be returned immediately but could be decorated by a defer
statement.
#3 would work as suggested by @josharian i.e. on a non-nil error the code would branch to the label. However, there would be no default error handler label as that case would now degenerate into #2.
It seems to me that this will usually be a better way of decorating errors (or handling them locally and then returning nil) than defer
as it’s simpler and quicker. Anybody who didn’t like it could still use #2.
It would be best practice to place the error handling label/code near the end of the function and not to jump back into the rest of the function body. However, I don’t think the compiler should enforce either as there might be odd occasions where they’re useful and enforcement might be difficult in any case.
So normal label and goto
behavior would apply subject (as @josharian said) to #26058 being fixed first but I think it should be fixed anyway.
The name of the label couldn’t be panic
as this would conflict with #4.
#4 would panic
immediately rather than returning or branching. Consequently, if this were the only version of try
used in a particular function, no ERP would be required.
I’ve added this so the testing package can work as it does now without the need for a further built-in or other changes. However, it might be useful in other fatal scenarios as well.
This needs to be a separate version of try
as the alternative of branching to an error handler and then panicking from that would still require an ERP.
Many of the counter-proposals posted to this issue suggesting other, more capable error-handling constructs duplicate existing language constructs, like the if statement. (Or they conflict with the goal of “making error checks more lightweight, reducing the amount of Go program text to error checking.” Or both.)
In general, Go already has a perfectly capable error-handling construct: the entire language, especially if statements. @DavexPro was right to refer back to the Go blog entry Errors are values. We need not design a whole separate sub-language concerned with errors, nor should we. I think the main insight over the past half year or so has been to remove “handle” from the “check/handle” proposal in favor of reusing what language we already have, including falling back to if statements where appropriate. This observation about doing as little as possible eliminates from consideration most of the ideas around further parameterizing a new construct.
With thanks to @brynbellomy for his many good comments, I will use his try-else as an illustrative example. Yes, we might write:
func doSomething() (int, error) {
// Inline error handler
a, b := try SomeFunc() else err {
return 0, errors.Wrap(err, "error in doSomething:")
}
// Named error handlers
handler logAndContinue err {
log.Errorf("non-critical error: %v", err)
}
handler annotateAndReturn err {
return 0, errors.Wrap(err, "error in doSomething:")
}
c, d := try SomeFunc() else logAndContinue
e, f := try OtherFunc() else annotateAndReturn
// ...
return 123, nil
}
but all things considered this is probably not a significant improvement over using existing language constructs:
func doSomething() (int, error) {
a, b, err := SomeFunc()
if err != nil {
return 0, errors.Wrap(err, "error in doSomething:")
}
// Named error handlers
logAndContinue := func(err error) {
log.Errorf("non-critical error: %v", err)
}
annotate:= func(err error) (int, error) {
return 0, errors.Wrap(err, "error in doSomething:")
}
c, d, err := SomeFunc()
if err != nil {
logAndContinue(err)
}
e, f, err := SomeFunc()
if err != nil {
return annotate(err)
}
// ...
return 123, nil
}
That is, continuing to rely on the existing language to write error handling logic seems preferable to creating a new statement, whether it’s try-else, try-goto, try-arrow, or anything else.
This is why try
is limited to the simple semantics if err != nil { return ..., err }
and nothing more: shorten the one common pattern but don’t try to reinvent all possible control flow. When an if statement or a helper function is appropriate, we fully expect people to continue to use them.
…
@griesemer With the error-handler variant of original try proposal, is the overarching function’s requirement for returning error now no longer required.
When I first enquired about it the err => panic, I was pointed out that the proposal considered it but considered it too dangerous (for good reason). But if we make the use of try()
without a error-handler in a scenario where overarching function doesn’t return error, making it into a compile-time error alleviates the concern discussed in the proposal
@pjebs The overarching function’s requirement for returning an error was not required in the original design if an error handler was provided. But it’s just another complication of try
. It is much better to keep it simple. Instead, it would be clearer to have a separate must
function, which always panics on error (but otherwise is like try
). Then it’s obvious what happens in the code and one doesn’t have to look at the context.
The main attraction of having such a must
would be that it could be used with unit tests; especially if the testing
package were suitably adjusted to recover from the panics caused by must
and report them as test failures in a nice way. But why add yet another new language mechanism when we can just adjust the testing package to also accept test function of the form TestXxx(t *testing.T) error
? If they return an error, which seems quite natural after all (perhaps we should have done this from the start), then try
will work just fine. Local tests will need a bit more work, but it’s probably doable.
The other relatively common use for must
is in global initialization expressions (must(regexp.Compile...
, etc.). If would be a “nice to have” but that does not necessarily raise it to the level required for a new language feature.
@griesemer Given that must
is vaguely related to try
, and given that the momentum is towards try
getting implemented, don’t you think it’s good to consider must
at the same time - even if it’s merely a “nice to have”.
The chances are that if it’s not discussed in this round, it simply won’t get implemented/seriously considered, at least for 3+ years (or perhaps ever). The overlap in discussion would also be good rather than starting from scratch and recycling discussions.
Many people have stated that must
compliments try
very nicely.
@pjebs It certainly doesn’t appear that there’s any “momentum towards try
getting implemented” right now… - And we also only just posted this two days ago. Nor has anything been decided. Let’s give this some time.
It has not escaped us that must
dovetails nicely with try
, but that’s not the same as making it part of the language. We only have started to explore this space with a wider group of people. We really don’t know yet what might come up in support or against it. Thanks.
if err != nil
everywhere, to having try
everywhere. It shifts the proposed problem and does not solve it.Although, I’d argue that the current error handling mechanism is not a problem to begin with. We just need to improve tooling and vetting around it.
Furthermore, I would argue that if err != nil
is actually more readable than try
because it does not clutter the line of the business logic language, rather sits right below it:
file := try(os.OpenFile("thing")) // less readable than,
file, err := os.OpenFile("thing")
if err != nil {
}
And if Go was to be more magical in its error handling, why not just totally own it. For example Go can implicitly call the builtin try
if a user does not assign an error. For example:
func getString() (string, error) { ... }
func caller() {
defer func() {
if err != nil { ... } // whether `err` must be defined or not is not shown in this example.
}
// would call try internally, because a user is not
// assigning an error value. Also, it can add a compile error
// for "defined and not used err value" if the user does not
// handle the error.
str := getString()
}
To me, that would actually accomplish the redundancy problem at the cost of magic and potential readability.
I’ve read the proposal and really like where try is going.
Given how prevalent try is going to be, I wonder if making it a more default behavior would make it easier to handle.
Consider maps. This is valid:
v := m[key]
as is this:
v, ok := m[key]
What if we handle errors exactly the way try suggests, but remove the builtin. So if we started with:
v, err := fn()
Instead of writing:
v := try(fn())
We could instead write:
v := fn()
When the err value is not captured, it gets handled exactly as the try does. Would take a little getting used to, but it feels very similar to v, ok := m[key]
and v, ok := x.(string)
. Basically, any unhandled error causes the function to return and the err value to be set.
To go back to the design docs conclusions and implementation requirements:
• The language syntax is retained and no new keywords are introduced
• It continues to be syntactic sugar like try and hopefully is easy to explain.
• Does not require new syntax
• It should be completely backward compatible.
I imagine this would have nearly the same implementation requirements as try as the primary difference is rather than the builtin triggering the syntactic sugar, now it’s the absence of the err field.
So using the CopyFile
example from the proposal along with defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
, we get:
func CopyFile(src, dst string) (err error) {
defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
r := os.Open(src)
defer r.Close()
w := os.Create(dst)
defer func() {
err := w.Close()
if err != nil {
os.Remove(dst) // only if a “try” fails
}
}()
io.Copy(w, r)
w.Close()
return nil
}
@savaki I like thi and was thinking about what it would take to make Go flip the error handling by always handling errors by default and let the programmer specify when not to do so (by capturing the err into a variable) but total lack of any identifier would make code hard to follow as one wouldn’t be able to see all the return points. May be a convention to name functions that could return an error differently could work (like capitalizing public identifiers). May be if a function returned an error, it must always end with, let’s say ?
. Then Go could always implicitly handle the error and automatically return it to the calling function just like try. This makes it very similar to some proposals suggesting to use a ?
identifier instead of try but an important difference is that here ?
would be part of the name of the function and not an additional identifier. In fact a function that returned error
as the last return value wouldn’t even compile if not suffixed ?
. Of course ?
is arbitrary and could be replaced with anything else that made the intent more explicit. operation?()
would be equivalent to wrapping try(someFunc())
but ?
would be part of the function name and it’s sole purpose would be to indicate that the function can return an error just like the capitalizing the first letter of a variable.
This superficially ends up being very similar to other proposals asking to replace try
with ?
but a critical difference is that makes error handling implicit (automatic) and instead makes ignoring (or wrapping) errors explicit which kind of a best practice anyway. The most obvious problem with this of course is that it is not backward compatible and I’m sure there are many more.
That said, I’d be very interested in seeing how Go can make error handling the default/implicit case by automating it and let the programmer write a bit of extra code to ignore/override the handling. The challenge I think is how to make all the return points obvious in this case because without it errors will become more like exceptions in the sense that they could come from anywhere as the flow of the program would not make it obvious. One could say making errors implicit with visual indicator is the same as implementing try
and making errcheck
a compiler failure.
could we do something like c++ exceptions with decorators for old functions?
func some_old_test() (int, error){
return 0, errors.New("err1")
}
func some_new_test() (int){
if true {
return 1
}
throw errors.New("err2")
}
func throw_res(int, e error) int {
if e != nil {
throw e
}
return int
}
func main() {
fmt.Println("Hello, playground")
try{
i := throw_res(some_old_test())
fmt.Println("i=", i + some_new_test())
} catch(err io.Error) {
return err
} catch(err error) {
fmt.Println("unknown err", err)
}
}
@owais I was thinking the semantics would be exactly the same as try so at the very least you would need to declare the err type. So if we started with:
func foo() error {
_, err := fn()
if err != nil {
return err
}
return nil
}
If I understand the try proposal, simply doing this:
func foo() error {
_ := fn()
return nil
}
would not compile. One nice perk is that it gives the compile an opportunity to tell the user what’s missing. Something to the effect that using implicit error handling requires the error return type to be named, err.
This then, would work:
func foo() (err error) {
_ := fn()
return nil
}
@savaki I think I understood your original comment and I like the idea of Go handling errors by default but I don’t think it’s viable without adding some additional syntax changes and once we do that, it becomes strikingly similar to the current proposal.
The biggest downside to what you propose is that it doesn’t expose all the points from where a function can return unlike current if err != nil {return err}
or the try function introduced in this proposal. Even though it would function exactly the same way under the hood, visually the code would look very different. When reading code, there would be no way of knowing which function calls might return an error. That would end up being a worse experience than exceptions IMO.
May be error handling could be made implicit if the compiler forced some semantic convention on functions that could return errors. Like they must start or end with a certain phrase or character. That’d make all return points very obvious and I think it’d be better than manual error handling but not sure how significantly better considering there are already lint checks that cry out load when they spot an error being ignored. It’d be very interesting to see if the compiler can force functions to be named a certain way depending on whether they could return possible errors.
@savaki I actually do like your proposal to omit try()
and allow it to be more like testing a map or a type assertion. That feels much more “Go-like.”
However, there is still one glaring issue I see, and that is your proposal presumes that all errors using this approach will trigger a return
and leave the function. What it does not contemplate is issuing a break
out of the current for
or a continue
for the current for
.
Early return
s are a sledgehammer when many times a scalpel is the better choice.
So I assert break
and continue
should be allowed to be valid error handling strategies and currently your proposal presumes only return
whereas try()
presumes that or calling an error handler that itself can only return
, not break
or continue
.
why not just handle the case of an error that isn’t assigned to a variable.
implicit return for the if err != nil case, compiler can generate local variable name for returns if necessary can’t be accessed by the programmer. personally I dislike this particular case from a code readability standapoint
f := os.Open("foo.txt")
prefer an explicit return, follows the code is read more than written mantra
f := os.Open("foo.txt") else return
interestingly we could accept both forms, and have gofmt automatically add the else return.
adding context, also local naming of the variable. return becomes explicit because we want to add context.
f := os.Open("foo.txt") else err {
return errors.Wrap(err, "some context")
}
adding context with multiple return values
f := os.Open("foo.txt") else err {
return i, j, errors.Wrap(err, "some context")
}
nested functions require that the outer functions handle all results in the same order minus the final error.
bits := ioutil.ReadAll(os.Open("foo")) else err {
// either error ends up here.
return i, j, errors.Wrap(err, "some context")
}
compiler refuses compilation due to missing error return value in function
func foo(s string) int {
i := strconv.Atoi(s) // cannot implicitly return error due to missing error return value for foo.
return i * 2
}
happily compiles because error is explicitly ignored.
func foo(s string) int {
i, _ := strconv.Atoi(s)
return i * 2
}
compiler is happy. it ignores the error as it currently does because no assignment or else suffix occurs.
func foo() error {
return errors.New("whoops")
}
func bar() {
foo()
}
within a loop you can use continue.
for _, s := range []string{"1","2","3","4","5","6"} {
i := strconv.Atoi(s) else continue
}
edit: replaced ;
with else
Looks like savaki and i had similar ideas, i just added the block semantics for dealing with the error if desired. For example adding comtext, for loops where you want to short circuit etc
@mikeschinkel see my extension, he and i had similar ideas i just extended it with an optional block statement
@james-lawrence
@mikesckinkel see my extension, he and i had similar ideas i just extended it with an optional block statement
Taking your example:
f := os.Open("foo.txt"); err {
return errors.Wrap(err, "some context")
}
Which compares to what we do today:
f,err := os.Open("foo.txt");
if err != nil {
return errors.Wrap(err, "some context")
}
Is definitely preferable to me. Except it has a few issues:
err
appears to be “magically” declared. Magic should be minimized, no? So let’s declare it:
f, err := os.Open("foo.txt"); err {
return errors.Wrap(err, "some context")
}
But that still does not work because Go does not interpret nil
values as false
nor pointer values as true
, so it would need to be:
f, err := os.Open("foo.txt"); err != nil {
return errors.Wrap(err, "some context")
}
And what that works, it starts to feel like just as much work and a lot of syntax on one line, so and I might continue to do the old way for clarity.
But what if Go added two (2) builtins; iserror()
and error()
? Then we could do this, which does not feel as bad to me:
f := os.Open("foo.txt"); iserror() {
return errors.Wrap(error(), "some context")
}
Or better (something like):
f := os.Open("foo.txt"); iserror() {
return error().Extend("some context")
}
What do you, and others think?
As an aside, check my username spelling. I would not have been notified of your mention if I wasn’t paying attention anyway…
@mikeschinkel sorry about the name I was on my phone and github wasn’t autosuggesting.
err appears to be “magically” declared. Magic should be minimized, no? So let’s declare it:
meh, the entire idea of automatically inserting a return is magical. this is hardly the most magical thing going on in this entire proposal. Plus I’d argue the err was declared; just at the end inside a scoped block’s context preventing it from polluting the parent scope while still maintaining all the good stuff we get normally with using if statements.
I’m generally pretty happy withs go’s error handling with the upcoming additions to the errors package. I don’t see anything in this proposal as super helpful. I’m just attempting to offer the most natural fit for the golang if we’re dead set on doing it.
“the entire idea of automatically inserting a return is magical.”
You won’t get any argument from me there.
“this is hardly the most magical thing going on in this entire proposal.”
I guess I was trying to argue that “all magic is problematic.”
“Plus I’d argue the err was declared; just at the end inside a scoped block’s context…”
So if I wanted to call it err2
this would work too?
f := os.Open("foo.txt"); err2 {
return errors.Wrap(err, "some context")
}
So I assume you are also proposing special case handling of the err
/err2
after the semi-colon, i.e. that it would be assumed to be either nil
or not nil
instead of bool
like when checking a map?
if _,ok := m[a]; !ok {
print("there is no 'a' in 'm'")
}
I’m generally pretty happy withs go’s error handling with the upcoming additions to the errors package.
I too am happy with error handling, when combined with break
and continue
(but not return
.)
As it is, I see this try()
proposal as more harmful than helpful, and would rather see nothing than this implement as-proposed. #jmtcw.
@mikeschinkel special handling would be that the block executes only when there is an error. ``` f := os.Open(‘foo’); err { return err } // err would always be non-nil here.
@mikeshenkel I see returning from a loop as a plus rather than a negative. My guess is that would encourage developers to either use a separate function to handle the contents of a loop or explicitly use err as we currently do. Both of these seem like good outcomes to me.
From my POV, I don’t feel like this try syntax has to handle every use case just like I don’t feel that I need to use the
V, ok:= m[key]
Form from reading from a map
“Both of these seem like good outcomes to me.”
We will have to agree to disagree here.
“this try syntax (does not have to) handle every use case”
That meme is probably the most troubling. At least given how resistant the Go team/community has been to any changes in the past that are not broadly applicable.
If we allow that justification here, why can’t we revisit past proposals that have been turned down because they were not broadly applicable?
And are we now open to argue for changes in Go that are just useful for selected edge cases?
In my guess, setting this precedent will not produce good outcomes long term…
”@mikeshenkel”
P.S. I did not see your message at first because of misspelling. (this does not offend me, I just don’t get notified when my username is misspelled…)
…
Therefore, I propose that we either truly solve the ‘problem’ like in the above example or keep the current error handling but instead of changing the language to solve redundancy and wrapping, we don’t change the language but we improve the tooling and vetting of code to make the experience better.
For example, in VSCode there’s a snippet called iferr
if you type it and hit enter, it expands to a full error handling statement…therefore, writing it never feels tiresome to me, and reading later on is better.
…
8) @marwan-at-work argues that try
simply shifts error handling from if
statements to try
expressions. Instead, if we want to actually solve the problem, Go should “own” error handling by making it truly implicit. The goal should be to make (proper) error handling simpler and developers more productive (@cpuguy).
…
8) We have considered looking at the problem from the error handling (handle
) point of view rather than from the error testing (try
) point of view. Specifically, we briefly considered only introducing the notion of an error handler (similar to the original design draft presented at last year’s Gophercon). The thinking was that if (and only if) a handler is declared, in multi-value assignments where the last value is of type error
, that value can simply be left away in an assignment. The compiler would implicitly check if it is non-nil, and if so branch to the handler. That would make explicit error handling disappear completely and encourage everybody to write a handler instead. This seemed to extreme an approach because it would be completely implicit - the fact that a check happens would be invisible.
…
One thing that was not addressed in your feedback was the idea of use the tooling/vetting part of the Go language to improve the error handling experience, rather then updating the Go syntax.
For example, with the landing of the new LSP (gopls
), it seems like a perfect place to analyze a function’s signature and taking care of the error handling boilerplate for the developer, with proper wrapping and vetting too.
@marwan-at-work I’m not sure what you are proposing that the tools are do for you. Do you suggest that they hide the error handling somehow?
@griesemer >I’m not sure what you are proposing that the tools are do for you. Do you suggest that they hide the error handling somehow?
Quite the opposite: I’m suggesting that gopls
can optionally write the error handling boilerplate for you.
As you mentioned in your last comment:
The reason for this proposal is that error handling (specifically the associated boilerplate code) was mentioned as a significant issue in Go (next to the lack of generics) by the Go community
So the heart of the problem is that the programmer ends up writing a lot of boilerplate code. So the issue is about writing, not reading. Therefore, my suggestion is: let the computer (tooling/gopls) do the writing for the programmer by analyzing the function signature and placing proper error handling clauses.
For example:
// user begins to write this function:
func openFile(path string) ([]byte, error) {
file, err := os.Open(path)
defer file.Close()
bts, err := ioutil.ReadAll(file)
return bts, nil
}
Then the user triggers the tool, perhaps by just saving the file (similar to how gofmt/goimports typically work) and gopls
would look at this function, analyze its return signature and augments the code to be this:
// user has triggered the tool (by saving the file, or code action)
func openFile(path string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("openFile: %w", err)
}
defer file.Close()
bts, err := ioutil.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("openFile: %w", err)
}
return bts, nil
}
This way, we get the best of both worlds: we get the readability/explicitness of the current error handling system, and the programmer did not write any error handling boilerplate. Even better, the user can go ahead and modify the error handling blocks later on to do different behavior: gopls
can understand that the block exists, and it wouldn’t modify it.
How would the tool know that I intended to handle the err
later on in the function instead of returning early? Albeit rare, but code I have written nonetheless.
@marwan-at-work > As you mentioned in your last comment: > > > The reason for this proposal is that error handling (specifically the associated boilerplate code) was mentioned as a significant issue in Go (next to the lack of generics) by the Go community > > So the heart of the problem is that the programmer ends up writing a lot of boilerplate code. So the issue is about writing, not reading.
I think it’s actually the other way round - for me the biggest annoyance with the current error handling boilerplate isn’t so much having to type it, but rather how it scatters the function’s happy path vertically across the screen, making it harder to understand at a glance. The effect is particularly pronounced in I/O-heavy code, where there’s usually a block of boilerplate between every two operations. Even a simplistic version of CopyFile
takes ~20 lines even though it really only performs five steps: open source, defer close source, open destination, copy source -> destination, close destination.
Another issue with the current syntax, is that, as I noted earlier, if you have a chain of operations each of which can return an error, the current syntax forces you to give names to all the intermediate results, even if you’d rather leave some anonymous. When this happens, it also hurts readability because you have to spend brain cycles parsing those names, even though they’re not very informative.
…
Backing up from gofmt to general tools, the suggestion to focus on tooling for writing error checks instead of a language change is equally problematic. As Abelson and Sussman put it, “Programs must be written for people to read, and only incidentally for machines to execute.” If machine tooling is required to cope with the language, then the language is not doing its job. Readability must not be limited to people using specific tools.
…
It doesn’t read like Go. People want assignment syntax, without the subsequent nil test, as that looks like Go. Thirteen separate responses to check/handle suggested this; see Recurring Themes here: https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes
f, # := os.Open(...) // return on error
f, #panic := os.Open(...) // panic on error
f, #hname := os.Open(...) // invoke named handler on error
// # is any available symbol or unambiguous pair
Nesting of function calls that return errors obscures the order of operations, and hinders debugging. The state of affairs when an error occurs, and therefore the call sequence, should be clear, but here it’s not:
@patrick-nyt is still another proponent of assignment syntax to trigger a nil test, in https://github.com/golang/go/issues/32437#issuecomment-499533464
This concept appears in 13 separate responses to the check/handle proposal https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes
f, ?return := os.Open(...)
f, ?panic := os.Open(...)
Why? Because it reads like Go 1, whereas try()
and check
do not.
…
EDIT: Thirdly, ideally, go language should gain generics first, where an important use case would be the ability to implement this try function as a generic, so the bikeshedding can end, and everyone can get the error handling that they prefer themselves.
@griesemer Thanks for your recap of the discussion.
I notice one point is missing in there: some people have argued that we should wait with this feature until we have generics, which will hopefully allow us to solve this problem in a more elegant way.
@beoran Perhaps you could expand on the connection between generics and error handling. When I think about them, they seem like two different things. Generics is not a catch all that can address all problems with the language. It is the ability to write a single function that can operate on multiple types.
This specific error handling proposal tries to reduce boilerplate by introducing a predeclared function try
that changes the flow control in some circumstances. Generics will never change the flow of control. So I really don’t see the relationship.
Too bad that you do not anticipate generics powerful enough to implement try, I actually would have hoped it would be possible to do so.
Yes, this proposal could be a first step, although I don’t see much use in it myself as it stands now.
Granted, this issue has perhaps too much focus on detailed alternatives, but it goes to show that many participants are not completely happy with it. What seems to be lacking is a wide consensus about this proposal…
@ianlancetaylor > @beoran Perhaps you could expand on the connection between generics and error handling.
Not speaking for @beoran but in my comment from a few minutes ago you’ll see that if we had generics (plus variadic return parameters) then we could build our own try()
.
However — and I will repeat what I said above about generics here where it will be easier to see:
” I think the use-cases for generics should be whittled down by adding builtins to address generic’s use-cases rather than add the confusing semantics and syntax salad of generics from Java et. al.)”
@ianlancetaylor
When trying to formulate an answer to your question, I tried to implement the try
function in Go as it is, and to my delight, it’s actually already possible to emulate something quite similar:
func try(v interface{}, err error) interface{} {
if err != nil {
panic(err)
}
return v
}
See here how it can be used: https://play.golang.org/p/Kq9Q0hZHlXL
The downsides to this approach are:
1. A deferred rescue is needed, but with try
as in this proposal, a deferred handler is also needed if we want to do proper error handling. So I feel this is not a serious downside. It could even be better if Go had some kind of super(arg1, ..., argn)
builtin causes the caller of the caller, one level up the call stack, to return with the given arguments arg1,…argn, a sort of super return if you will.
2. This try
I implemented can only work with a function that returns a single result and an error.
3. You have to type assert the returned emtpy interface results.
Sufficiently powerful generics could resolve problem 2 and 3, leaving only 1, which could be resolved by adding a super()
. With those two features in place, we could get something like:
func (T ... interface{})try(T, err error) super {
if err != nil {
super(err)
}
super(T...)
}
And then the deferred rescue would not be needed anymore. This benefit would be available even if no generics are added to Go.
Actually, this idea of a super() builtin is so powerful and interesting I might post a proposal for it separately.
@beoran Good to see we came to exactly the same constraints independently regarding implementing try()
in userland, except for the super part which I did not include because I wanted to talk about something similar in an alternate proposal. :-)
@beoran @mikeschinkel Earlier I suggested that we could not implement this version of try
using generics, because it changes the control flow. If I’m reading correctly, you are both suggesting that we could use generics to implement try
by having it call panic
. But this version of try
very explicitly does not panic
. So we can’t use generics to implement this version of try
.
Yes, we could use generics (a version of generics significantly more powerful than the one in the design draft at https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md) to write a function that panics on error. But panicking on error is not the kind of error handling that Go programmers write today, and it doesn’t seem like a good idea to me.
@ianlancetaylor
“Yes, we could use generics … But panicking on error is not the kind of error handling that Go programmers write today, and it doesn’t seem like a good idea to me.”
I actually strongly agree with you on this thus it appears you may have misinterpreted the intent of my comment. I was not at all suggesting that the Go team would implement error handling that used panic()
— of course not.
Instead I was trying to actually follow your lead from many of your past comments on other issues and suggested that we avoid making any changes to Go that are not absolutely necessary because they are instead possible in userland. So if generics were addressed then people who would want try()
could in fact implement it themselves, albeit by leveraging panic()
. And that would be one less feature that the team would need to add to and document for Go.
What I was not doing — and maybe that was not clear — was advocating that people actually use panic()
to implement try()
, just that they could if they really wanted to, and they had the features of generics.
Does that clarify?
To me calling panic
, however that is done, is quite different from this proposal for try
. So while I think I understand what you are saying, I do not agree that they are equivalent. Even if we had generics powerful enough to implement a version of try
that panics, I think there would still be a reasonable desire for the version of try
presented in this proposal.
@ianlancetaylor Acknowledged. Again, I was looking for a reason that try()
would not need to be added rather than find a way to add it. As I said above, I would far rather not have anything new for error handling than have try()
as proposed here.
The issue I have with this is it assumes you always want to just return the error when it happens. When maybe you want to add context to the error and the return it or maybe you just want to behave differently when an error happens. Maybe that is depending on the type of error returned.
I would prefer something akin to a try/catch which might look like
Assuming foo()
defined as
func foo() (int, error) {}
You could then do
n := try(foo()) {
case FirstError:
// do something based on FirstError
case OtherError:
// do something based on OtherError
default:
// default behavior for any other error
}
Which translates to
n, err := foo()
if errors.Is(err, FirstError) {
// do something based on FirstError
if errors.Is(err, OtherError) {
// do something based on OtherError
} else {
// default behavior for any other error
}
@MrTravisB
The issue I have with this is it assumes you always want to just return the error when it happens.
I disagree. It assumes that you want to do so often enough to warrant a shorthand for just that. If you don’t, it doesn’t get in the way of handling errors plainly.
When maybe you want to add context to the error and the return it or maybe you just want to behave differently when an error happens.
The proposal describes a pattern for adding block-wide context to errors. @josharian pointed out that there is an error in the examples, though, and it’s not clear what the best way is to avoid it. I have written a couple of examples of ways to handle it.
For more specific error context, again, try
does a thing, and if you don’t want that thing, don’t use try
.
@boomlinde Exactly my point. This proposal is trying to solve a singular use case rather than providing a tool to solve the larger issue of error handling. I think the fundamental question if exactly what you pointed out.
It assumes that you want to do so often enough to warrant a shorthand for just that.
In my opinion and experience this use case is a small minority and doesn’t warrant shorthand syntax.
Also, the approach of using defer
to handle errors has issues in that it assumes you want to handle all possible errors the same. defer
statements can’t be canceled.
defer fmt.HandleErrorf(&err, “foobar”)
n := try(foo())
x : try(foo2())
What if I want different error handling for errors that might be returned from foo()
vs foo2()
?
@MrTravisB >What if I want different error handling for errors that might be returned from foo() vs foo2()?
Then you use something else. That’s the point @boomlinde was making.
@MrTravisB
Exactly my point. This proposal is trying to solve a singular use case rather than providing a tool to solve the larger issue of error handling. I think the fundamental question if exactly what you pointed out.
What specifically did I say that is exactly your point? It rather seems to me that you fundamentally misunderstood my point if you think that we agree.
In my opinion and experience this use case is a small minority and doesn’t warrant shorthand syntax.
In the Go source there are thousands of cases that could be handled by try
out of the box even if there was no way to add context to errors. If minor, it’s still a common cause of complaint.
Also, the approach of using defer to handle errors has issues in that it assumes you want to handle all possible errors the same. defer statements can’t be canceled.
Similarly, the approach of using + to handle arithmetic assumes that you don’t want to subtract, so you don’t if you don’t. The interesting question is whether block-wide error context at least represents a common pattern.
What if I want different error handling for errors that might be returned from foo() vs foo2()
Again, then you don’t use try
. Then you gain nothing from try
, but you also don’t lose anything.
@boomlinde The point that we agree on is that this proposal is trying to solving a minor use case and the fact that “if you don’t need, don’t use it” is the primary argument for it furthers that point. As @elagergren-spideroak stated, that argument doesn’t work because even if I don’t want to use it, others will which forces me to use it. By the logic of your argument Go should also have a ternary statement. And if you don’t like ternary statements, don’t use them.
Disclaimer - I do think Go should have a ternary statement but given that Go’s approach to language features is to not introduce features which could make code more difficult to read then it shouldn’t.
…
A question about the proposal that is probably best answered by “nope”: How does try
interact with variadic arguments? It’s the first case of a variadic (ish) function that doesn’t have its variadic-nes in the last argument. Is this allowed:
var e []error
try(e...)
Leaving aside why you’d ever do that. I suspect the answer is “no” (otherwise the follow-up is “what if the length of the expanded slice is 0). Just bringing that up so it can be kept in mind when phrasing the spec eventually.
…
try()
in userlandUnless I misunderstand the proposal - which I probably do — here is try()
in the Go Playground implemented in userland, albeit with just one (1) return value and returning an interface instead of the expected type:
package main
import (
"errors"
"fmt"
"strings"
)
func main() {
defer func() {
r := recover()
if r != nil && strings.HasPrefix(r.(string),"TRY:") {
fmt.Printf("Ouch! %s",strings.TrimPrefix(r.(string),"TRY: "))
}
}()
n := try(badjuju()).(int)
fmt.Printf("Just chillin %dx!",n)
}
func badjuju() (int,error) {
return 10, errors.New("this is a really bad error")
}
func try(args ...interface{}) interface{} {
err,ok := args[1].(error)
if ok && err != nil {
panic(fmt.Sprintf("TRY: %s",err.Error()))
}
return args[0]
}
So the user could add a try2()
, try3()
and so on depending on how many return values they needed to return.
But Go would only need one (1) simple yet universal language feature to allow users who want try()
to roll their own support, albeit one that still requires explicit type assertion. Add a (fully backward-compatible) capability for a Go func
to return a variadic number of return values, e.g.:
func try(args ...interface{}) ...interface{} {
err,ok := args[1].(error)
if ok && err != nil {
panic(fmt.Sprintf("TRY: %s",err.Error()))
}
return args[0:len(args)-2]
}
And if you address generics first then the type assertions would not even be necessary (although I think the use-cases for generics should be whittled down by adding builtins to address generic’s use-cases rather than add the confusing semantics and syntax salad of generics from Java et. al.)
…
I want to bring up what i see as a minor issue with the way try is defined in the proposal and how it might impact future extensions.
Try is defined as a function taking a variable number of arguments.
func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)
Passing the result of a function call to try
as in try(f())
works implicitly due to the way multiple return values work in Go.
By my reading of the proposal, the following snippets are both valid and semantically equivalent.
a, b = try(f())
//
u, v, err := f()
a, b = try(u, v, err)
The proposal also raises the possibility of extending try
with extra arguments.
If we determine down the road that having some form of explicitly provided error handler function, or any other additional parameter for that matter, is a good idea, it is trivially possible to pass that additional argument to a try call.
Suppose we want to add a handler argument. It can either go at the beginning or end of the argument list.
var h handler
a, b = try(h, f())
// or
a, b = try(f(), h)
Putting it at the beginning doesn’t work, because (given the semantics above) try
wouldn’t be able to distinguish between an explicit handler argument and a function that returns a handler.
func f() (handler, error) { ... }
func g() (error) { ... }
try(f())
try(h, g())
Putting it at the end would probably work, but then try would be unique in the language as being the only function with a varargs parameter at the beginning of the argument list.
Neither of these problems is a showstopper, but they do make try
feel inconsistent with the rest of the language, and so i’m not sure try
would be easy to extend in the future as the proposal states.
@magical You’re feedback has been addressed in the updated version of the detailed proposal.
@magical
Having a handler is powerful, perhaps: I you already declared h,
you can
var h handler
a, b, h = f()
or
a, b, h.err = f()
if its a function-like:
h:= handler(err error){
log(...)
return ....
}
Then there was asuggetion to
a, b, h(err) = f()
All can invoke the handler And you can also “select” handler that returns or only captures the error (conitnue/break/return) as some suggested.
And thus the varargs issue is gone.
@magical Thanks for pointing this out. I noticed the same shortly after posting the proposal (but didn’t bring it up to not further cause confusion). You are correct that as is, try
couldn’t be extended. Luckily the fix is easy enough. (As it happens our earlier internal proposals didn’t have this problem - it got introduced when I rewrote our final version for publication and tried to simplify try
to match existing parameter passing rules more closely. It seemed like a nice - but as it turns out, flawed, and mostly useless - benefit to be able to write try(a, b, c, handle)
.)
An earlier version of try
defined it roughly as follows: try(expr, handler)
takes one (or perhaps two) expressions as arguments, where the first expression may be multi-valued (can only happen if the expression is a function call). The last value of that (possibly multi-valued) expression must be of type error
, and that value is tested against nil. (etc. - the rest you can imagine).
Anyway, the point is that try
syntactically accepts only one, or perhaps two expressions. (But it’s a bit harder to describe the semantics of try
.) The consequence would be that code such as:
a, b := try(u, v, err)
would not be permitted anymore. But there is little reason for making this work in the first place: In most cases (unless a
and b
are named results) this code - if important for some reason - could be rewritten easily into
a, b := u, v // we don't care if the assignment happens in case of an error
try(err)
(or use an if
statement as needed). But again, this seems unimportant.
@griesemer Thanks for the explanation. That’s the conclusion i came to as well.
that return statement will have to enumerate all the (typically zero) result values since a naked return is not permitted in this case
A naked return is not permitted, but try would be. One thing I like about try (either as a function or a statement) is that I will not need to think how to set non error values when returning an error any more, I will just use try.
Readability seems to be an issue but what about go fmt presenting try() so that it stands out, something like:
f := try(
os.Open("file.txt")
)
…
7) @pierrec suggested that gofmt
could format try
expressions suitably to make them more visible.
Alternatively, one could make existing code more compact by allowing gofmt
to format if
statements checking for errors on one line (@zeebo).
…
7) Using gofmt
to format try
expressions such that they are extra visible would certainly be an option. But it would take away from some of the benefits of try
when used in an expression.
The suggestion to gofmt every try call into multiple lines directly conflicts with the goal of “making error checks more lightweight, reducing the amount of Go program text to error checking.”
…
Allow statements like
if err != nil {
return nil, 0, err
}
to be formatted on one line by gofmt
when the block only contains a return
statement and that statement does not contain newlines. For example:
if err != nil { return nil, 0, err }
gofmt
keeps newlines if they already exist (like struct literals). Opt in also allows the writer to make some error handling be emphasizedgofmt
return
statements, so it won’t be abused to golf code unnecessarilytry
expressions handles this poorlytry
leans more towards the writertry
existing on multiple lines. For example this comment or this comment which introduces a style likef, err := os.Open(file)
try(maybeWrap(err))
err
value is being returned. Therefore, I suspect this form will be commonly used. Allowing one lined if blocks is almost the same thing, except it’s also explicit about what the return values aredefer
based wrapping. Both raise the barrier to wrapping errors and the former may require godoc
changestry
versus using traditional error handlingtry
or something else in the future. The change may be positive even if try
is acceptedtesting
library or main
functions. In fact, if the proposal allows any single lined statement instead of just returns, it may reduce usage of assertion based libraries. Considervalue, err := something()
if err != nil { t.Fatal(err) }
n, err := src.Read(buf)
if err == io.EOF { return nil }
else if err != nil { return err }
In summary, this proposal has a small cost, can be designed to be opt-in, doesn’t preclude any further changes since it’s stylistic only, and reduces the pain of reading verbose error handling code while keeping everything explicit. I think it should at least be considered as a first step before going all in on try
.
Some examples ported
func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
try(dbfile.RunMigrations(db, dbMigrations))
t := &Thing{
thingy: thingy,
scanner: try(newScanner(thingy, db, client)),
}
t.initOtherThing()
return t, nil
}
func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
err := dbfile.RunMigrations(db, dbMigrations))
if err != nil { return nil, fmt.Errorf("running migrations: %v", err) }
t := &Thing{thingy: thingy}
t.scanner, err = newScanner(thingy, db, client)
if err != nil { return nil, fmt.Errorf("creating scanner: %v", err) }
t.initOtherThing()
return t, nil
}
It’s competitive in space usage while still allowing for adding context to errors.
func (c *Config) Build() error {
pkgPath := try(c.load())
b := bytes.NewBuffer(nil)
try(emplates.ExecuteTemplate(b, "main", c))
buf := try(format.Source(b.Bytes()))
target := fmt.Sprintf("%s.go", filename(pkgPath))
try(ioutil.WriteFile(target, buf, 0644))
// ...
}
func (c *Config) Build() error {
pkgPath, err := c.load()
if err != nil { return nil, errors.WithMessage(err, "load config dir") }
b := bytes.NewBuffer(nil)
err = templates.ExecuteTemplate(b, "main", c)
if err != nil { return nil, errors.WithMessage(err, "execute main template") }
buf, err := format.Source(b.Bytes())
if err != nil { return nil, errors.WithMessage(err, "format main template") }
target := fmt.Sprintf("%s.go", filename(pkgPath))
err = ioutil.WriteFile(target, buf, 0644)
if err != nil { return nil, errors.WithMessagef(err, "write file %s", target) }
// ...
}
The original comment used a hypothetical tryf
to attach the formatting, which has been removed. It’s unclear the best way to add all the distinct contexts, and perhaps try
wouldn’t even be applicable.
@zeebo Yep, I’m into that. @kungfusheep ’s article used a one line err check like that and I got exited to try it out. Then as soon as I save, gofmt expanded it into three lines which was sad. Many functions in the stdlib are defined in one line like that so it surprised me that gofmt would expand that out.
@qrpnxz
I happen to read a lot of go code. One of the best things about the language is the ease that comes from most code following a particular style (thanks gofmt).
I don’t want to read a bunch of code wrapped in try(f())
.
This means there will either be a divergence in code style/practice, or linters like “oh you should have used try()
here” (which again I don’t even like, which is the point of me and others commenting on this proposal).
It is not objectively better than if err != nil { return err }
, just less to type.
It is not objectively better than
if err != nil { return err }
, just less to type.
There is one objective difference between the two: try(Foo())
is an expression. For some, that difference is a downside (the try(strconv.Atoi(x))+try(strconv.Atoi(y))
criticism). For others, that difference is an upside for much the same reason. Still not objectively better or worse - but I also don’t think the difference should be swept under the rug and claiming that it’s “just less to type” doesn’t do the proposal justice.
@Merovius The proposal really is just a syntax sugar macro though, so it’s gonna end up being about what people think looks nicer or is going to cause the least trouble. If you think not, please explain to me. That’s why I’m for it, personally. It’s a nice addition without adding any keywords from my perspective.
It is not objectively better than
if err != nil { return err }
, just less to type.There is one objective difference between the two:
try(Foo())
is an expression. For some, that difference is a downside (thetry(strconv.Atoi(x))+try(strconv.Atoi(y))
criticism). For others, that difference is an upside for much the same reason. Still not objectively better or worse - but I also don’t think the difference should be swept under the rug and claiming that it’s “just less to type” doesn’t do the proposal justice.
This is one of the biggest reasons I like this syntax; it lets me use an error-returning function as part of a larger expression without having to name all the intermediate results. In some situations naming them is easy, but in others there’s no particularly meaningful or non-redundant name to give them, in which case I’d rather much not give them a name at all.
Thank you very much @griesemer for taking the time to go through everyone’s ideas and explicitly providing thoughts. I think that it really helps with the perception that the community is being heard in the process.
@pierrec suggested that gofmt could format try expressions suitably to make them more visible. Alternatively, one could make existing code more compact by allowing gofmt to format if statements checking for errors on one line (@zeebo).
Using
gofmt
to formattry
expressions such that they are extra visible would certainly be an option. But it would take away from some of the benefits oftry
when used in an expression.
These are valuable thoughts about requiring gofmt
to format try
, but I’m interested if there are any thoughts in particular on gofmt
allowing the if
statement checking the error to be one line. The proposal was lumped in with formatting of try
, but I think it’s a completely orthogonal thing. Thanks.
@zeebo It would be easy to make gofmt
format if err != nil { return ...., err }
on a single line. Presumably it would only be for this specific kind of if
pattern, not all “short” if
statements?
Along the same lines, there were concerns about try
being invisible because it’s on the same line as the business logic. We have all these options:
Current style:
a, b, c, ... err := BusinessLogic(...)
if err != nil {
return ..., err
}
One-line if
:
a, b, c, ... err := BusinessLogic(...)
if err != nil { return ..., err }
try
on a separate line (!):
a, b, c, ... err := BusinessLogic(...)
try(err)
try
as proposed:
a, b, c := try(BusinessLogic(...))
The first and the last line seem the clearest (to me), especially once one is used to recognize try
as what it is. With the last line, an error is explicitly checked for, but since it’s (usually) not the main action, it is a bit more in the background.
@griesemer Thanks. Indeed, the proposal was to only be for return
, but I suspect allowing any single statement to be a single line would be good. For example in a test one could, with no changes to the testing library, have
if err != nil { t.Fatal(err) }
The first and the last line seem the clearest (to me), especially once one is used to recognize try as what it is. With the last line, an error is explicitly checked for, but since it’s (usually) not the main action, it is a bit more in the background.
With the last line, some of the cost is hidden. If you want to annotate the error, which I believe the community has vocally said is desired best practice and should be encouraged, one would have to change the function signature to name the arguments and hope that a single defer
applied to every exit in the function body, otherwise try
has no value; perhaps even negative due to its ease.
I don’t have any more to add that I believe hasn’t already been said.
@zeebo: The examples I gave are 1:1 translations. The first (traditional if
) didn’t handle the error, and so didn’t the others. If the first handled the error, and if this were the only place an error is checked for in a function, the first example (using an if
) might be the appropriate choice of writing the code. If there’s multiple error checks, all of which use the same error handling (wrapping), say because they all add information about the current function, one could use a defer
statement to handle the errors all in one place. Optionally, one could rewrite the if
’s into try
’s (or leave them alone). If there are multiple errors to check, and they all handle the errors differently (which might be a sign that the function’s concern is too broad and that it might need to be split up), using if
’s is the way to go. Yes, there is more than one way to do the same thing, and the right choice depends on the code as well as on personal taste. While we do strive in Go for “one way to do one thing”, this is of course already not the case, especially for common constructs. For instance, when an if
-else
-if
sequence becomes too long, sometimes a switch
might be more appropriate. Sometimes a variable declaration var x int
expresses intent better than x := 0
, and so forth (though not everybody is happy about this).
Regarding your question about the “rewrite”: No, there wouldn’t be a compilation error. Note that the rewrite happens internally (and may be more efficient than the code pattern suggest), and there is no need for the compiler to complain about a shadowed return. In your example, you declared a local err
variable in a nested scope. try
would still have direct access to the result err
variable, of course. The rewrite might look more like this under the covers.
…
I understand the examples were translations that didn’t annotate the errors. I attempted to argue that try
makes it harder to do good annotation of errors in common situations, and that error annotation is very important to the community. A large portion of the comments thus far have been exploring ways to add better annotation support to try
.
About having to handle the errors differently, I disagree that it’s a sign that the function’s concern is too broad. I’ve been translating some examples of claimed real code from the comments and placing them in a dropdown at the bottom of my original comment, and the example in https://github.com/golang/go/issues/32437#issuecomment-499007288 I think demonstrates a common case well:
func (c *Config) Build() error {
pkgPath, err := c.load()
if err != nil { return nil, errors.WithMessage(err, "load config dir") }
b := bytes.NewBuffer(nil)
err = templates.ExecuteTemplate(b, "main", c)
if err != nil { return nil, errors.WithMessage(err, "execute main template") }
buf, err := format.Source(b.Bytes())
if err != nil { return nil, errors.WithMessage(err, "format main template") }
target := fmt.Sprintf("%s.go", filename(pkgPath))
err = ioutil.WriteFile(target, buf, 0644)
if err != nil { return nil, errors.WithMessagef(err, "write file %s", target) }
// ...
}
That function’s purpose is to execute a template on some data into a file. I don’t believe it needs to be split up, and it would be unfortunate if all of those errors just gained the line that they were created on from a defer. That may be alright for developers, but it’s much less useful for users.
I think it’s also a bit of a signal how subtle the defer wrap(&err, "message: %v", err)
bugs were and how they tripped up even experienced Go programmers.
To summarize my argument: I think error annotation is more important than expression based error checking, and we can get quite a bit of noise reduction by allowing statement based error checking to be one line instead of three. Thanks.
@zeebo Thanks for this example. It looks like using an if
statement is exactly the right choice in this case. But point taken, formatting the if’s into one-liners may streamline this a bit.
…
go fmt
didn’t rewrite single line if
statements. Personally, I would prefer a simple rule that this would be allowed for any single statement if
whether concerned with error handling or not. In fact I have never been able to understand why this isn’t currently allowed when writing single-line functions where the body is placed on the same line as the declaration is allowed.…
4) Acknowledged.
Looking simply at the length of the current proposed syntax versus what is available now, the case where the error just needs to be returned without handling it or decorated it is the case where most convenience is gained. An example with my favorite syntax so far:
try a, b := f() else err { return fmt.Errorf("Decorated: %s", err); }
if a,b, err :=f; err != nil { return fmt.Errorf("Decorated: %s", err); }
try a, b := f()
if a,b, err :=f; err != nil { return err; }
So, different from what I thought before, maybe it is simply enough to alter go fmt, at least for the decorated/handled error case. While for the just pass on the error case, something like try might still be desirable as syntactic sugar for this very common use case.
Just interjecting a specific comment since I did not see anyone else explicitly mention it, specifically about changing gofmt
to support the following single line formatting, or any variant:
if f() { return nil, err }
Please, no. If we want a single line if
then please make a single line if
, e.g.:
if f() then return nil, err
But please, please, please do not embrace syntax salad removing the line breaks that make it easier to read code that uses braces.
Should go fmt allow single-line if
but not case, for, else, var ()
? I’d like them all, please ;-)
The Go team has turned aside many requests for single-line error checks.
on err, return err
statements could be repetitive, but they’re explicit, terse, and clear.
…
The suggestion to gofmt an error-testing if statement in a single line also directly conflicts with this goal. The error checks do not become substantially more lightweight nor reduced in amount by removing the interior newline characters. If anything, they become more difficult to skim.
The main benefit of try is to have a clear abbreviation for the one most common case, making the unusual ones stand out more as worth reading carefully.
Thanks @rsc for providing your thoughts on allowing if
statements to be gofmt’d as a single line.
The suggestion to gofmt an error-testing if statement in a single line also directly conflicts with this goal. The error checks do not become substantially more lightweight nor reduced in amount by removing the interior newline characters. If anything, they become more difficult to skim.
I estimate these assertions differently.
I find reducing the number of lines from 3 to 1 to be substantially more lightweight. Wouldn’t gofmt requiring an if statement to contain, for example, 9 (or even 5) newlines instead of 3 be substantially more heavyweight? It’s the same factor (amount) of reduction/expansion. I’d argue that struct literals have this exact trade-off, and with the addition of try
, will allow control flow just as much as an if
statement.
Secondly, I find the argument that they become more difficult to skim to apply equally well to try
, if not more. At least an if
statement would have to be on it’s own line. But perhaps I misunderstand what is meant by “skim” in this context. I’m using it to mean “mostly skip over but be aware of.”
All that said, the gofmt suggestion was predicated on taking an even more conservative step than try
and has no impact on try
unless it would be sufficient. It sounds like it’s not, and so if I want to discuss it more I’ll open a new issue/proposal. :+1:
I find reducing the number of lines from 3 to 1 to be substantially more lightweight.
I think everyone agrees that it is possible for code to too dense. For example if your entire package is one line I think we all agree that’s a problem. We all probably disagree on the precise line. For me, we’ve established
n, err := src.Read(buf)
if err == io.EOF {
return nil
} else if err != nil {
return err
}
as the way to format that code, and I think it would be quite jarring to try to shift to your example
n, err := src.Read(buf)
if err == io.EOF { return nil }
else if err != nil { return err }
instead. If we’d started out that way, I’m sure it would be fine. But we didn’t, and it’s not where we are now.
Personally, I do find the former lighter weight on the page in the sense that it is easier to skim. You can see the if-else at a glance without reading any actual letters. In contrast, the denser version is hard to tell at a glance from a sequence of three statements, meaning you have to look more carefully before its meaning becomes clear.
In the end, it’s OK if we draw the denseness-vs-readability line in different places as far as number of newlines. The try proposal is focused on not just removing newlines but removing the constructs entirely, and that produces a lighter-weight page presence separate from the gofmt question.
An unintended consequence of try()
may be that projects abandon go fmt in order to gain single-line error checks. That’s almost all the benefits of try()
with none of the costs. I’ve done that for a few years; it works well.
But I’d rather be able to define a last-resort error handler for the package, and eliminate all the error checks which need it. What I’d define is not try()
.
@networkimprov, you seem to be coming from a different position than the Go users we are targeting, and your message would contribute more to the conversation if it carried additional detail or links so we can better understand your point of view.
It’s unclear what “costs” you believe try has. And while you say that abandoning gofmt has “none of the costs” of try (whatever those are), you seem to be ignoring that gofmt’s formatting is the one used by all programs that help rewrite Go source code, like goimports, eg, gorename, and so on. You abandon go fmt at the cost of abandoning those helpers, or at least putting up with substantial incidental edits to your code when you invoke them. Even so, if it works well for you to do so, that’s great: by all means keep doing that.
It’s also unclear what “define a last-resort error handler for the package” means or why it would be appropriate to apply an error-handling policy to an entire package instead of to a single function at a time. If the main thing you’d want to do in an error handler is add context, the same context would not be appropriate across the entire package.
@rsc, to your questions,
My package-level handler of last resort – when error is not expected:
func quit(err error) {
fmt.Fprintf(os.Stderr, "quit after %s\n", err)
debug.PrintStack() // because panic(err) produces a pile of noise
os.Exit(3)
}
Context: I make heavy use of os.File (where I’ve found two bugs: #26650 & #32088)
A package-level decorator adding basic context would need a caller
argument – a generated struct which provides the results of runtime.Caller().
I wish the go fmt rewriter would use existing formatting, or let you specify formatting per transformation. I make do with other tools.
The costs (i.e. drawbacks) of try()
are well documented above.
I’m honestly floored that the Go team offered us first check/handle
(charitably, a novel idea), and then the ternaryesque try()
. I don’t see why you didn’t issue an RFP re error handling, and then collect community comment on some of the resulting proposals (see #29860). There’s a lot of wisdom out here you could leverage!
@networkimprov, re https://github.com/golang/go/issues/32437#issuecomment-502879351
I’m honestly floored that the Go team offered us first check/handle (charitably, a novel idea), and then the ternaryesque try(). I don’t see why you didn’t issue an RFP re error handling, and then collect community comment on some of the resulting proposals (see #29860). There’s a lot of wisdom out here you could leverage!
As we discussed in #29860, I honestly don’t see much difference between what you are suggesting we should have done as far as soliciting community feedback and what we actually did. The draft designs page explicitly says they are “starting points for discussion, with an eventual goal of producing designs good enough to be turned into actual proposals.” And people did write many things ranging from short feedback to full alternate proposals. And most of it was helpful and I appreciate your help in particular in organizing and summarizing. You seem to be fixated on calling it a different name or introducing additional layers of bureaucracy, which as we discussed on that issue we don’t really see a need for.
But please don’t claim that we somehow did not solicit community advice or ignored it. That’s simply not true.
I also can’t see how try is in any way “ternaryesque”, whatever that would mean.
@rsc, apologies for veering off-topic! I raised package-level handlers in https://github.com/golang/go/issues/32437#issuecomment-502840914 and responded to your request for clarification in https://github.com/golang/go/issues/32437#issuecomment-502879351
I see package-level handlers as a feature that virtually everyone could get behind.
@rsc, apologies for veering off-topic! I raised package-level handlers in #32437 (comment) and responded to your request for clarification in #32437 (comment)
I see package-level handlers as a feature that virtually everyone could get behind.
I don’t see what ties together the concept of a package with specific error handling. It’s hard to imagine the concept of a package-level handler being useful to, say, net/http
. In a similar vein, despite writing smaller packages than net/http
in general, I cannot think of a single use case where I would have preferred a package-level construct to do error handling. In general, I’ve found that the assumption that everyone shares one’s experiences, use cases, and opinions is a dangerous one :)
I apologize if this has been brought up before, but I couldn’t find any mention of it.
try(DoSomething())
reads well to me, and makes sense: the code is trying to do something. try(err)
, OTOH, feels a little off, semantically speaking: how does one try an error? In my mind, one could test or check an error, but trying one doesn’t seem right.
I do realize that allowing try(err)
is important for reasons of consistency: I suppose it would be strange if try(DoSomething())
worked, but err := DoSomething(); try(err)
didn’t. Still, it feels like try(err)
looks a little awkward on the page. I can’t think of any other built-in functions that can be made to look this strange so easily.
I do not have any concrete suggestions on the matter, but I nevertheless wanted to make this observation.
I like try
on separate line.
And I hope that it can specify handler
func independently.
func try(error, optional func(error)error)
func (p *pgStore) DoWork() error {
tx, err := p.handle.Begin()
try(err)
handle := func(err error) error {
tx.Rollback()
return err
}
var res int64
_, err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
try(err, handle)
_, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
try(err, handle)
return tx.Commit()
}
While repetitiveif err !=nil { return ... err }
is certainly an ugly stutter, I’m with those
who think the try() proposal is very low on readability and somewhat inexplicit.
The use of named returns is problematic too.
If this sort of tidying is needed, why not try(err)
as syntactic sugar for
if err !=nil { return err }
:
file, err := os.Open("file.go")
try(err)
for
file, err := os.Open("file.go")
if err != nil {
return err
}
And if there is more than one return value, try(err)
could return t1, ... tn, err
where t1, … tn are the zero values of the other return values.
This suggestion can obviate the need for named return values and be, in my view, easier to understand and more readable.
Even better, I think would be:
file, try(err) := os.Open("file.go")
Or even
file, err? := os.Open("file.go")
This last is backwards compatible (? is currently not allowed in identifiers).
(This suggestion is related to https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes. But the recurring theme examples seem different because that was at a stage when an explicit handle was still being discussed instead of leaving that to a defer.)
Thanks to the go team for this careful, interesting proposal.
@patrick-nyt What you are suggesting:
file, err := os.Open("file.go")
try(err)
will be possible with the current proposal.
One objection to try
seems to be that it is an expression. Suppose instead that there is a unary postfix statement ?
that means return if not nil. Here is the standard code sample (assuming that my proposed deferred package is added):
func CopyFile(src, dst string) error {
var err error // Don't need a named return because err is explicitly named
defer deferred.Annotate(&err, "copy %s %s", src, dst)
r, err := os.Open(src)
err?
defer deferred.AnnotatedExec(&err, r.Close)
w, err := os.Create(dst)
err?
defer deferred.AnnotatedExec(&err, r.Close)
defer deferred.Cond(&err, func(){ os.Remove(dst) })
_, err = io.Copy(w, r)
return err
}
The pgStore example:
func (p *pgStore) DoWork() error {
tx, err := p.handle.Begin()
err?
defer deferred.Cond(&err, func(){ tx.Rollback() })
var res int64
err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
// tricky bit: this would not change the value of err
// but the deferred.Cond would still be triggered by err being set before
deferred.Format(err, "insert table")?
_, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
deferred.Format(err, "insert table2")?
return tx.Commit()
}
try
as a keyword certainly helps with readability (vs. a function call) and seems less complex. @brynbellomy @crawshaw thank you for taking the time to write out the examples.
I suppose my general thought is that try
does too much. It solves: call function, assign variables, check error, and return error if exists. I propose we instead cut scope and solve only for conditional return: “return if last arg not nil”.
This is probably not a new idea… But after skimming the proposals in the error feedback wiki, I did not find it (does not mean it’s not there)
Mini proposal on conditional return
excerpt:
err, thing := newThing(name)
refuse nil, err
I added it to the wiki as well under “alternative ideas”
Doing nothing also seems like a very reasonable option.
@alexhornbake that gives me an idea slightly different that would be more useful
assert(nil, err)
assert(len(a), len(b))
assert(true, condition)
assert(expected, given)
this way it would not just apply to error checking, but to many types of logic errors.
The given would be wrapped in an error and returned.
@alexhornbake
Just as try
is not actually trying, refuse
is not actually “refusing”. The common intent here has been that we are setting a “protective relay” (relay
is short, accurate, and alliterative to return
) that “trips” when one of the wired values meets a condition (i.e. is non-nil error). It’s a sort of circuit breaker and, I believe, can add value if it’s design was limited to uninteresting cases to simply reduce some of the lowest-hanging boilerplate. Anything remotely complex should rely on plain Go code.
@daved I’m not sure I follow. Do you dislike the name, or dislike the idea?
Anyway, I posted this here as an alternative… If there is legit interest I can open a new issue for discussion, don’t want to pollute this thread (maybe too late?) Thumbs up/down on the original idea will give us a sense… Of course open to alternative names. Important part is “conditional return without trying to handle assignment”.
If an extra line is usually required for decoration/wrap, let’s just “allocate” a line for it.
f, err := os.Open(path) // normal Go \o/
on err, return fmt.Errorf("Cannot open %s, due to %v", path, err)
@networkimprov I think I could like that as well. Pushing a more alliterative and descriptive term I already brought up…
f, err := os.Open(path)
relay err { nil, fmt.Errorf("Cannot open %s, due to %v", path, err) }
// marginally shorter, doesn't trigger vertical formatting unless excessively wide
// enclosed expression restricted to a list of values that match the return args
or
f, err := os.Open(path)
relay(err) nil, fmt.Errorf("Cannot open %s, due to %v", path, err)
// somewhere between statement and func, prob more pleasing to type w/out completion
// trailing expression restricted to a list of values that match the return args
// maybe excessive width triggers linting noise - with a reformatter available
// providing a reformatter would make swapping old (narrow enough) code easy
@daved glad you like it! on err, ...
would allow any single-stmt handler:
err := f() // followed by one of
on err, return err // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err) // doesn't stop the function
on err, continue // retry in a loop
on err, hname err // named handler invocation without parens
on err, ignore err // logs error if handle ignore() defined
handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
if err == io.Bad { return err } // non-local return
fmt.Println(clr, err)
}
EDIT: on
borrows from Javascript. I didn’t want to overload if
.
A comma isn’t essential, but I don’t like semicolon there. Maybe colon?
I don’t quite follow relay
; it means return-on-error?
A protective relay is tripped when some condition is met. In this case, when an error value is not nil, the relay alters the control flow to return using the subsequent values.
*I wouldn’t want to overload ,
for this case, and am not a fan of the term on
, but I like the premise and overall look of the code structure.
@networkimprov @daved I don’t dislike these two ideas, but they don’t feel like enough of an improvement over simply allowing single-line if err != nil { ... }
statements to warrant a language change. Also, does it do anything to reduce repetitive boilerplate in the case where you’re simply returning the error? Or is the idea that you always have to write out the return
?
@brynbellomy In my example, there is no return
. relay
is a protective relay defined as “if this err is not nil, the following will be returned”.
Using my second example from earlier:
f, err := os.Open(path)
relay(err) nil, fmt.Errorf("Cannot open %s, due to %v", path, err)
Could also be something like:
f, err := os.Open(path)
relay(err)
With the error that trips the relay being returned along with zero values for other return values (or whatever values are set for named returned values). Another form that might be useful:
wrap := func(err error, msg string) error {
if err != nil {
fmt.Errorf("%s: %s", msg, err)
}
return nil
}
// ...
f, err := os.Open(path)
relay(err, wrap(err, fmt.Sprintf("Cannot open %s", path)))
Where the second relay arg is not called unless the relay is tripped by the first relay arg. The optional second relay error arg would be the value returned.
@rsc
> Syntax
>
> This discussion has identified six different syntaxes to write the same semantics from the proposal:
>
> * f := try(os.Open(file))
, from the proposal (builtin function)
> * f := try os.Open(file)
, using a keyword (prefix keyword)
> * f := os.Open(file)?
, like in Rust (call-suffix operator)
> * f := os.Open?(file)
, suggested by @rogpeppe (call-infix operator)
> * try f := os.Open(file)
, suggested by @thepudds (try statement)
> * try ( f := os.Open(file); f.Close() )
, suggested by @bakul (try block)
try {error} {optional wrap func} {optional return args in brackets}
f, err := os.Open(file)
try err wrap { a, b }
… and, IMO, improving readability (through alliteration) as well as semantic accuracy:
f, err := os.Open(file)
relay err
or
f, err := os.Open(file)
relay err wrap
or
f, err := os.Open(file)
relay err wrap { a, b }
or
f, err := os.Open(file)
relay err { a, b }
I know advocating for relay versus try is easy to dismiss as off-topic, but I can just imagine attempting to explain how try is not trying anything and doesn’t throw anything. It’s not clear AND has baggage. relay
being a new term would allow a clear-minded explanation, and the description has a basis in circuitry (which is what this is all about anyway).
Edit to clarify: Try can mean - 1. to experience something and then judge it subjectively 2. to verify something objectively 3. attempt to do something 4. fire off multiple control flows that can be interrupted and launch an interceptable notification if so
In this proposal, try is doing none of those. We are actually running a function. It is then rewiring the control flow based upon an error value. This is literally the definition of a protective relay. We are directly re-laying circuitry (i.e. short-circuiting the current function scope) according to the value of a tested error.
@daved, I’m glad the “protective relay” analogy works for you. It doesn’t work for me. Programs are not circuits.
Any word can be misunderstood: “break” does not break your program. “continue” doesn’t continue execution at the next statement like normal. “goto” … well goto is impossible to misunderstand actually. :-)
https://www.google.com/search?q=define+try says “make an attempt or effort to do something” and “subject to trial”. Both of those apply to “f := try(os.Open(file))“. It attempts to do the os.Open (or, it subjects the error result to trial), and if the attempt (or the error result) fails, it returns from the function.
We used check last August. That was a good word too. We switched to try, despite the historical baggage of C++/Java/Python, because the current meaning of try in this proposal matches the meaning in Swift’s try (without the surrounding do-catch) and in Rust’s original try!. It won’t be terrible if we decide later that check is the right word after all but for now we should focus on things other than the name.
“break” does not break your program. “continue” doesn’t continue execution at the next statement like normal. “goto” … well goto is impossible to misunderstand actually. :-)
break
does break the loop. continue
does continue the loop, and goto
does go to the indicated destination. Ultimately, I do hear you, but please consider what happens when a function mostly completes and returns an error, but does not rollback. It was not a try/trial. I do think check
is far superior in that regard (to “halt the progress of” through “examination” is certainly apt).
More pertinent, I am curious about the form of try/check that I offered as opposed to the other syntaxes.
try {error} {optional wrap func} {optional return args in brackets}
f, err := os.Open(file)
try err wrap { a, b }
I am curious about the form of try/check that I offered as opposed to the other syntaxes.
I think that form ends up recreating existing control structures.
Agreed, I think that was my goal; I don’t think more complex mechanisms are worthwhile. If I were in your shoes, the most I’d offer is a bit of syntactic sugar to silence the majority of complaints and no more.
I like this a lot more than I liked august version.
I think that much of the negative feedback, that isn’t outright opposed to returns without the return
keyword, can be summarized in two points:
See for example:
The rebuttal for those two objections is respectively:
try
” / it’s not going to be appropriate for 100% of casesI don’t really have anything to say about 1 (I don’t feel strongly about it). But regarding 2 I’d note that the august proposal didn’t have this problem, most counter proposals also don’t have this problem.
In particular neither the tryf
counter-proposal (that’s been posted independently twice in this thread) nor the try(X, handlefn)
counter-proposal (that was part of the design iterations) had this problem.
I think it’s hard to argue that try
, as it, is will push people away from decorating errors with relavant context and towards a single generic per-function error decoration.
Because of these reasons I think it’s worth trying to address this issue and I want to propose a possible solution:
Currently the parameter of defer
can only be a function or method call. Allow defer
to also have a function name or a function literal, i.e.
defer func(...) {...}
defer packageName.functionName
When panic or deferreturn encounter this type of defer they will call the function passing the zero value for all their parameters
Allow try
to have more than one parameter
When try
encounters the new type of defer it will call the function passing a pointer to the error value as the first parameter followed by all of try
’s own parameters, except the first one.
For example, given:
func errorfn() error {
return errors.New("an error")
}
func f(fail bool) {
defer func(err *error, a, b, c int) {
fmt.Printf("a=%d b=%d c=%d\n", a, b, c)
}
if fail {
try(errorfn, 1, 2, 3)
}
}
the following will happen:
f(false) // prints "a=0 b=0 c=0"
f(true) // prints "a=1 b=2 c=3"
The code in https://github.com/golang/go/issues/32437#issuecomment-499309304 by @zeebo could then be rewritten as:
func (c *Config) Build() error {
defer func(err *error, msg string, args ...interface{}) {
if *err == nil || msg == "" {
return
}
*err = errors.WithMessagef(err, msg, args...)
}
pkgPath := try(c.load(), "load config dir")
b := bytes.NewBuffer(nil)
try(templates.ExecuteTemplate(b, "main", c), "execute main template")
buf := try(format.Source(b.Bytes()), "format main template")
target := fmt.Sprintf("%s.go", filename(pkgPath))
try(ioutil.WriteFile(target, buf, 0644), "write file %s", target)
// ...
}
And defining ErrorHandlef as:
func HandleErrorf(err *error, format string, args ...interface{}) {
if *err != nil && format != "" {
*err = fmt.Errorf(format + ": %v", append(args, *err)...)
}
}
would give everyone the much sought after tryf
for free, without pulling fmt
-style format strings into the core language.
This feature is backwards compatible because defer
doesn’t allow function expressions as its argument. It doesn’t introduce new keywords.
The changes that need to be made to implement it, in addition to the ones outlined in https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md, are:
try
match the signature of the functions passed to defer
try
to copy its arguments into the arguments of the deferred call when it encounters a call deferred by the new kind of defer.@aarzilli - So according to your proposal, is a defer clause mandatory every time we give extra parameters to tryf
?
What happens if I do
try(ioutil.WriteFile(target, buf, 0644), "write file %s", target)
and do not write a defer function ?
@agnivade > What happens if I do (…) and do not write a defer function ?
typecheck error.
@aarzilli Thanks for your suggestion.
As long as decorating errors is optional, people will lean towards not doing it (it’s extra work after all). See also my comment here.
So, I don’t think the proposed try
discourages people from decorating errors (they are already discouraged even with the if
for the above reason); it’s that try
doesn’t encourage it.
(One way of encouraging it is to tie it into try
: One can only use try
if one also decorates the error, or explicitly opts out.)
But back to your suggestions: I think you’re introducing a whole lot more machinery here. Changing the semantics of defer
just to make it work better for try
is not something we would want to consider unless those defer
changes are beneficial in a more general way. Also, your suggestion ties defer
together with try
and thus makes both those mechanisms less orthogonal; something we would want to avoid.
But more importantly, I doubt you would want to force everybody to write a defer
just so they can use try
. But without doing that, we’re back to square one: people will lean towards not decorating errors.
If the goals are (reading https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md) : - eliminate the boilerplate - minimal language changes - covering “most common scenarios” - adding very little complexity to the language
I would take the suggestion give it an angle and allow “small steps” code migration for all the billions lines of code out there.
instead of the suggested:
func printSum(a, b string) error {
defer fmt.HandleErrorf(&err, "sum %s %s: %v", a,b, err)
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
fmt.Println("result:", x + y)
return nil
}
We can:
func printSum(a, b string) error {
var err ErrHandler{HandleFunc : twoStringsErr("printSum",a,b)}
x, err.Error := strconv.Atoi(a)
y,err.Error := strconv.Atoi(b)
fmt.Println("result:", x + y)
return nil
}
What would we gain? twoStringsErr can be inlined to printSum, or a general handler that knows how to capture errors (in this case with 2 string parameters) - so if I have same repeating func signatures used in many of my functions, I dont need t rewrite the handler each time in the same manner, I can have the ErrHandler type extended in the manner of:
type ioErrHandler ErrHandler
func (i ErrHandler) Handle() ...{
}
or
type parseErrHandler ErrHandler
func (p parseErrHandler) Handle() ...{
}
or
type str2IntErrHandler ErrHandler
func (s str2IntErrHandler) Handle() ...{
}
and use this all around my my code:
func printSum(a, b string) error {
var pErr str2IntErrHandler
x, err.Error := strconv.Atoi(a)
y,err.Error := strconv.Atoi(b)
fmt.Println("result:", x + y)
return nil
}
So, the actual need would be to develop a trigger when err.Error is set to not nil Using this method we can also:
func (s str2IntErrHandler) Handle() bool{
**return false**
}
Which would tell the calling function to continue instead of return
And use different error handlers in the same function:
func printSum(a, b string) error {
var pErr str2IntErrHandler
var oErr overflowError
x, err.Error := strconv.Atoi(a)
y,err.Error := strconv.Atoi(b)
fmt.Println("result:", x + y)
totalAsByte,oErr := sumBytes(x,y)
sunAsByte,oErr := subtractBytes(x,y)
return nil
}
etc.
Going over the goals again - eliminate the boilerplate - done - minimal language changes - done - covering “most common scenarios” - more than the suggeted IMO - adding very little complexity to the language - sone Plus - easier code migration from
x, err := strconv.Atoi(a)
to
x, err.Error := strconv.Atoi(a)
and actually - better readability (IMO, again)
@guybrand you are the latest adherent to this recurring theme (which I like).
See https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes
@guybrand That seems like an entirely different proposal; I think you should file it as its own issue so that this one can focus on discussing @griesemer’s proposal.
@dpinela > @guybrand That seems like an entirely different proposal; I think you should file it as its own issue so that this one can focus on discussing @griesemer’s proposal.
IMO my proposal differs only in syntax, meaning:
so the main diff is whether we’re wrapping the original function call with try(func()) that would always analyze the last var to jnz the call or use the actual return value to do that.
I know it looks diff, but actually very similar in concept. On the other hand - if you take the usual try …. catch in many c-like languages - that would be a very different implementation, different readability etc.
I do however seriously think of writing a proposal, thanks for the idea.
@griesemer
> “Personally, I like the error handling code out of the way (at the end)”
+1 to that!
I find that error handling implemented before the error occurs is much harder to reason about than error handling implemented after the error occurs. Having to mentally jump back and force to follow the logic flow feels like I am back in 1980 writing Basic with GOTOs.
Let me propose yet another potential way to handle errors using CopyFile()
as the example again:
``` func CopyFile(src, dst string) (err error) {
r := os.Open(src)
defer r.Close()
w := os.Create(dst)
defer w.Close()
io.Copy(w, r)
w.Close()
for err := error {
switch err.Source() {
…
Note that allowing the non-capturing of the last “error” from func
calls would allow later refactoring to return errors from func
s that initially did not need to return errors. And it would allow this refactoring without breaking any existing code that uses this form of error recovery and calls said func
s.
To me, that decision of “Should I return an error or forgo error handling for calling simplicity?” is one of my biggest quandaries when writing Go code. Allowing non-capturing of “errors” above would all but eliminate that quandary.
@griesemer I’m sure that this is not well thought through, but I tried to modify your suggestion closer to something that I’d be comfortable with here: https://www.reddit.com/r/golang/comments/bwvyhe/proposal_a_builtin_go_error_check_function_try/eq22bqa?utm_source=share&utm_medium=web2x
…
To attack this issue from a new direction, here’s an idea which I don’t think closely aligns with anything that has been discussed in the design document or this issue comment thread. Let’s call it “error-defers”:
Allow defer to be used to call functions with an implicit error parameter.
So if you have a function
func f(err error, t1 T1, t2 T2, ..., tn Tn) error
Then, in a function g
where the last result parameter has type error
(i.e., any function where try
my be used), a call to f
may be deferred as follows:
func g() (R0, R0, ..., error) {
defer f(t0, t1, ..., tn) // err is implicit
}
The semantics of error-defer are:
f
is called with the last result parameter of g
as the first input parameter of f
f
is only called if that error is not nilf
is assigned to the last result parameter of g
So to use an example from the old error-handling design doc, using error-defer and try, we could do
func printSum(a, b string) error {
defer func(err error) error {
return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
}()
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
fmt.Println("result:", x+y)
return nil
}
Here’s how HandleErrorf would work:
func printSum(a, b string) error {
defer handleErrorf("printSum(%q + %q)", a, b)
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
fmt.Println("result:", x+y)
return nil
}
func handleErrorf(err error, format string, args ...interface{}) error {
return fmt.Errorf(format+": %v", append(args, err)...)
}
One corner case that would need to be worked out is how to handle cases where it’s ambiguous which form of defer we are using. I think that only happens with (very unusual) functions with signatures like this:
func(error, ...error) error
It seems reasonable to say that this case is handled in the non-error-defer way (and this preserves backward compatibility).
Thinking about this idea for the last couple of days, it is a little bit magical, but the avoidance of named result parameters is a large advantage in its favor. Since try
encourages more use of defer
for error manipulation, it makes some sense that defer
could be extended to better suit it to that purpose. Also, there’s a certain symmetry between try
and error-defer.
Finally, error-defers are useful today even without try, since they supplant the use of named result parameters for manipulating error returns. For example, here’s an edited version of some real code:
// GetMulti retrieves multiple files through the cache at once and returns its
// results as a slice parallel to the input.
func (c *FileCache) GetMulti(keys []string) (_ []*File, err error) {
files := make([]*file, len(keys))
defer func() {
if err != nil {
// Return any successfully retrieved files.
for _, f := range files {
if f != nil {
c.put(f)
}
}
}
}()
// ...
}
With error-defer, this becomes:
// GetMulti retrieves multiple files through the cache at once and returns its
// results as a slice parallel to the input.
func (c *FileCache) GetMulti(keys []string) ([]*File, error) {
files := make([]*file, len(keys))
defer func(err error) error {
// Return any successfully retrieved files.
for _, f := range files {
if f != nil {
c.put(f)
}
}
return err
}()
// ...
}
…
err == nil
is problematicAs further digression, I want to bring up the concern I have felt about idiomatic error handling in Go. While I am a huge believer of Go’s philosophy to handle errors when they occur vs. using exception handling I feel the use of nil
to indicate no error is problematic because I often find I would like to return a success message from a routine — for use in API responses — and not only return a non-nil value just when there is an error.
So for Go 2 I would really like to see Go consider adding a new builtin type of status
and three builtin functions iserror()
, iswarning()
, issuccess()
. status
could implement error
— allowing for much backward compatibility and a nil
value passed to issuccess()
would return true
— but status
would have an additional internal state for error level so that testing for error level would always be done with one of the builtin functions and ideally never with a nil
check. That would allow something like the following approach instead:
func (me *Config) WriteFile() (sts status) {
for range only.Once {
var j []byte
j, sts = json.MarshalIndent(me, "", " ")
if iserror(sts) {
sts.AddMessage("unable to marshal config")
break
}
sts = me.MaybeMakeDir(me.GetDir(), os.ModePerm)
if iserror(sts) {
sts.AddMessage("unable to make directory'%s'", me.GetDir())
break
}
sts = ioutil.WriteFile(string(me.GetFilepath()), j, os.ModePerm)
if iserror(sts) {
sts.AddMessage("unable to write to config file '%s'",
me.GetFilepath(),
)
break
}
sts = fmt.Status("config file written")
}
return sts
}
I am already using a userland approach in a pre-beta level currently internal-use package that is similar to the above for error handling. Frankly I spend a lot less time thinking about how to structure code when using this approach than when I was trying to follow idiomatic Go error handling.
If you think there is any chance of evolving idiomatic Go code to this approach, please take it into consideration when implementing error handling, including when considering this try()
proposal.
…
Currently an “error” is defined as any object that implements Error() string
and of course is not nil
.
However, there is often a need to extend error even on success to allow returning values needed for success results of a RESTful API. So I would ask that the Go team please not automatically assume err!=nil
means “error” but instead check if an error object implements an IsError()
and if IsError()
returns true
before assuming that any non-nil
value is an “error.”
_(I am not necessarily talking about code in the standard library but primarily if you choose your control flow to branch on an “error”. If you only look at err!=nil
we will be very limited in what we can do in terms of return values in our functions.)_
BTW, allowing everyone to test for an “error” the same way could probably most easily be done by adding a new builtin iserror()
function:
type ErrorIser interface {
IsError() bool
}
func iserror(err error) bool {
if err == nil {
return false
}
if _,ok := err.(ErrorIser); !ok {
return true
}
return err.IsError()
}
I like the proposal but the fact you had to explicitly specify that defer try(...)
and go try(...)
are disallowed made me think something was not quite right…. Orthogonality is a good design guide. On further reading and seeing things like
x = try(foo(...))
y = try(bar(...))
I wonder if may be try
needs to be a context! Consider:
try (
x = foo(...)
y = bar(...)
)
Here foo()
and bar()
return two values, the second of which is error
. Try semantics only matter for calls within the try
block where the returned error value is elided (no receiver) as opposed ignored (receiver is _
). You can even handle some errors in between foo
and bar
calls.
Summary:
a) the problem of disallowing try
for go
and defer
disappears by virtue of the syntax.
b) error handling of multiple functions can be factored out.
c) its magic nature is better expressed as special syntax than as a function call.
If try is a context then we’ve just made try/catch blocks which we’re specifically trying to avoid (and for good reason)
There is no catch. Exact same code would be generated as when the current proposal has
x = try(foo(...))
y = try(bar(...))
This is just different syntax, not semantics. ````
I guess I had made a few assumptions about it that I shouldn’t have done, although there’s still a couple drawbacks to it.
What if foo or bar do not return an error, can they be placed in the try context as well? If not, that seems like it’d be kinda ugly to switch between error and non-error functions, and if they can, then we fall back to the issues of try blocks in older languages.
The second thing is that ypically the keyword ( ... )
syntax means you prefix the keyword on each line. So for import, var, const, etc: each line starts with the keyword. Making an exception to that rule doesn’t seem like a good decision
@deanveloper, the try
block semantics only matter for functions that return an error value and where the error value is not assigned. So the last example of present proposal could also be written as
try(x = foo(...))
try(y = bar(...))
putting both statements within the same block is simiar to what we do for repeatedimport
, const
and var
statements.
Now if you have, e.g.
try(
x = foo(...))
go zee(...)
defer fum()
y = bar(...)
)
This is equivalent to writing
try(x = foo(...))
go zee(...)
defer fum()
try(y = bar(...))
Factoring out all of that in one try block makes it less busy.
Consider
try(x = foo())
If foo() doesn’t return an error value, this is equivalent to
x = foo()
Consider
try(f, _ := os.open(filename))
Since returned error value is ignored, this is equivalent to just
f, _ := os.open(filename)
Consider
try(f, err := os.open(filename))
Since returned error value is not ignored, this is equivalent to
f, err := os.open(filename)
if err != nil {
return ..., err
}
As currently specified in the proposal.
And it also nicely declutters nested trys!
@ChrisHines To your point (which is echoed elsewhere in this thread), let’s add another restriction:
try
statement (even those without a handler) must occur on its own line.You would still benefit from a big reduction in visual noise. Then, you have guaranteed returns annotated by return
and conditional returns annotated by try
, and those keywords always stand at the beginning of a line (or at worst, directly after a variable assignment).
So, none of this type of nonsense:
try EmitEvent(try (try DecodeMsg(m)).SaveToDB())
but rather this:
dm := try DecodeMsg(m)
um := try dm.SaveToDB()
try EmitEvent(um)
which still feels clearer than this:
dm, err := DecodeMsg(m)
if err != nil {
return nil, err
}
um, err := dm.SaveToDB()
if err != nil {
return nil, err
}
err = EmitEvent(um)
if err != nil {
return nil, err
}
One thing I like about this design is that it is impossible to silently ignore errors without still annotating that one might occur. Whereas right now, you sometimes see x, _ := SomeFunc()
(what is the ignored return value? an error? something else?), now you have to annotate clearly:
x := try SomeFunc() else err {}
One of the strongest type of reactions to the initial proposal was concern around losing easy visibility of normal flow of where a function returns.
For example, @deanveloper expressed that concern very well in https://github.com/golang/go/issues/32437#issuecomment-498932961, which I think is the highest upvoted comment here.
@dominikh wrote in https://github.com/golang/go/issues/32437#issuecomment-499067357:
In gofmt’ed code, a return always matches /^\t*return / – it’s a very trivial pattern to spot by eye, without any assistance. try, on the other hand, can occur anywhere in the code, nested arbitrarily deep in function calls. No amount of training will make us be able to immediately spot all control flow in a function without tool assistance.
To help with that, @brynbellomy suggested yesterday: > any try statement (even those without a handler) must occur on its own line.
Taking that further, the try
could be required to be the start of the line, even for an assignment.
So it could be:
try dm := DecodeMsg(m)
try um := dm.SaveToDB()
try EmitEvent(um)
rather than the following (from @brynbellomy’s example):
dm, err := DecodeMsg(m)
if err != nil {
return nil, err
}
um, err := dm.SaveToDB()
if err != nil {
return nil, err
}
err = EmitEvent(um)
if err != nil {
return nil, err
}
That seems it would preserve a fair amount of visibility, even without any editor or IDE assistance, while still reducing boilerplate.
That could work with the currently proposed defer-based approach that relies on named result parameters, or it could work with specifying normal handler functions. (Specifying handler functions without requiring named return values seems better to me than requiring named return values, but that is a separate point).
The proposal includes this example:
info := try(try(os.Open(file)).Stat()) // proposed try built-in
That could instead be:
try f := os.Open(file)
try info := f.Stat()
That is still a reduction in boilerplate compared to what someone might write today, even if not quite as short as the proposed syntax. Perhaps that would be sufficiently short?
@elagergren-spideroak supplied this example:
try(try(try(to()).parse().this)).easily())
I think that has mismatched parens, which is perhaps a deliberate point or a subtle bit of humor, so I’m not sure if that example intends to have 2 try
or 3 try
. In any event, perhaps it would be better to require spreading that across 2-3 lines that start with try
.
@thepudds, this is what I was getting at in my earlier comment. Except that given
try f := os.Open(file)
try info := f.Stat()
An obvious thing to do is to think of try
as a try block where more than one sentence can be put within parentheses . So the above can become
try (
f := os.Open(file)
into := f.Stat()
)
If the compiler knows how to deal with this, the same thing works for nesting as well. So now the above can become
try info := os.Open(file).Stat()
From function signatures the compiler knows that Open can return an error value and as it is in a try block, it needs to generate error handling and then call Stat() on the primary returned value and so on.
The next thing is to allow statements where either there is no error value being generated or is handled locally. So you can now say
try (
f := os.Open(file)
debug("f: %v\n", f) // debug returns nothing
into := f.Stat()
)
This allows evolving code without having rearrange try blocks. But for some strange reason people seem to think that error handling must be explicitly spelled out! They want
try(try(try(to()).parse()).this)).easily())
While I am perfectly fine with
try to().parse().this().easily()
Even though in both cases exactly the same error checking code can be generated. My view is that you can always write special code for error handling if you need to. try
(or whatever you prefer to call it) simply declutters the default error handling (which is to punt it to the caller).
Another benefit is that if the compiler generates the default error handling, it can add some more identifying information so you know which of the four functions above failed.
If you make try a statement, you could use a flag to indicate which return value, and what action:
try c, @ := newRawConn(fd) // return
try c, @panic := newRawConn(fd) // panic
try c, @hname := newRawConn(fd) // invoke named handler
try c, @_ := newRawConn(fd) // ignore, or invoke "ignored" handler if defined
You still need a sub-expression syntax (Russ has stated it’s a requirement), at least for panic and ignore actions.
I’m happy to lob my support behind the last few posts where try
(the keyword) has been moved to the beginning of the line. It really ought to share the same visual space as return
.
I second @daved ‘s response. In my opinion each example that @crawshaw highlighted became less clear and more error prone as a result of try
.
A brief comment on try
as a statement: as I think can be seen in the example in https://github.com/golang/go/issues/32437#issuecomment-501035322, the try
buries the lede. Code becomes a series of try
statements, which obscures what the code is actually doing.
Existing code may reuse a newly declared error variable after the if err != nil
block. Hiding the variable would break that, and adding a named return variable to the function signature won’t always fix it.
Maybe it’s best to leave error declaration/assignment as is, and find a one-line error handling stmt.
err := f() // followed by one of
on err, return err // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err) // doesn't stop the function
on err, hname err // handler invocation without parens
on err, ignore err // optional ignore handler logs error if defined
if err, return err // alternatively, use if
handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
if err == io.Bad { return err }
fmt.Println(clr.name, err)
}
A try
subexpression could panic, meaning error is never expected. A variant of that could ignore any error.
f(try g()) // panic on error
f(try_ g()) // ignore any error
Try as a statement does reduce the boilerplate significantly, and more than try as an expression, if we allow it to work on a block of expressions as was proposed before, even without allowing an else block or an error handler. Using this, deandeveloper’s example becomes:
try (
name := FileName()
file := os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0600)
port := r.ReadString("\n")
lengthStr := r.ReadString("\n")
length := strconv.Atoi(lengthStr)
con := net.Dial("tcp", "localhost:"+port)
io.CopyN(os.Stdout, con, length)
)
If the goal is to reduce the if err!= nil {return err}
boilerplate, then I think statement try that allows to take a block of code has the most potential to do that, without becoming unclear.
@beoran At that point, why having try at all? Just allow an assignment where the last error value is missing and make it behave like if it was a try statement (or function call). Not that I am proposing it, but it would reduce boilerplate even more.
I think that the boilerplate would be efficiently reduced by these var blocks, but I fear it may lead to a huge amount of code becoming indented an additional level, which would be unfortunate.
Well, I think we’d still need the try for backwards compatibility, and also to be explicit about a return that can happen in the block. But note that I’m just following the logic of reducing the boiler plate and then seeing where it leads us. There’s always a tension between reducing boilerplate and clarity. I think the main issue at hand in this issue is that we all seem to disagree where the balance should be.
As for the indents, that’s what go fmt is for, so personally I don’t feel it’s much of a problem.
…
@ianlancetaylor I’ll absolutely concede that we’ll end up with dozens of lines prefixed by try
. However, I don’t see how that’s worse than dozens of lines postfixed by a two-to-four line conditional expression explicitly stating the same return
expression. Actually, try
(with else
clauses) makes it a bit easier to spot when an error handler is doing something special/non-default. Also, tangentially, re: conditional if
expressions, I think that they bury the lede more than the proposed try
-as-a-statement: the function call lives on the same line as the conditional, the conditional itself winds up at the very end of an already-crowded line, and the variable assignments are scoped to the block (which necessitates a different syntax if you need those variables after the block).
…
@beoran This is elegant, and when you think about it, it’s actually semantically different from other languages’ try
blocks, as it has only one possible outcome: returning from the function with an unhandled error. However: 1) this is probably confusing to new Go coders who have worked with those other languages (honestly not my biggest concern; I trust in the intelligence of programmers), and 2) this will lead to huge amounts of code being indented across many codebases. As far as my code is concerned, I even tend to avoid the existing type
/const
/var
blocks for this reason. Also, the only keywords that currently allow blocks like this are definitions, not control statements.
@yiyus I disagree with removing the keyword, as explicitness is (in my opinion) one of Go’s virtues. But I would agree that indenting huge amounts of code to take advantage of try
expressions is a bad idea. So maybe no try
blocks at all?
@yiyus https://github.com/golang/go/issues/32437#issuecomment-501139662
@beoran At that point, why having try at all? Just allow an assignment where the last error value is missing and make it behave like if it was a try statement (or function call). Not that I am proposing it, but it would reduce boilerplate even more.
That can’t be done because Go1 already allows calling a func foo() error
as just foo()
. Adding , error
to the return values of the caller would change behavior of existing code inside that function. See https://github.com/golang/go/issues/32437#issuecomment-500289410
I like the try a,b := foo()
instead of if err!=nil {return err}
because it replace a boilerplate for really simple case. But for everything else which add context do we really need something else than if err!=nil {...}
(it will be very difficult to find better) ?
Here is a link to the alternate proposal I mentioned above:
It calls for adding two (2) small but general purpose language features to address the same use cases as try()
func
/closure in an assignment statement.break
, continue
or return
more than one level.With this two features their would be no “magic” and I believe their usage would produce Go code that is easier to understand and more inline with the idiomatic Go code we are all familiar with.
echoing what @ubombi said:
try() function call interrupts code execution in the parent function.; there is no return keyword, but the code actually returns.
In Ruby, procs and lambdas are an example of what try
does…A proc is a block of code that its return statement returns not from the block itself, but from the caller.
This is exactly what try
does…it’s just a pre-defined Ruby proc.
I think if we were going to go that route, maybe we can actually let the user define their own try
function by introducing proc functions
I still prefer if err != nil
, because it’s more readable but I think try
would be more beneficial if the user defined their own proc:
proc try(err *error, msg string) {
if *err != nil {
*err = fmt.Errorf("%v: %w", msg, *err)
return
}
}
And then you can call it:
func someFunc() (string, error) {
err := doSomething()
try(&err, "someFunc failed")
}
The benefit here, is that you get to define error handling in your own terms. And you can also make a proc
exposed, private, or internal.
It’s also better than the handle {}
clause in the original Go2 proposal because you can define this only once for the entire codebase and not in each function.
One consideration for readability, is that a func() and a proc() might be called differently such as func()
and proc!()
so that a programmer knows that a proc call might actually return out of the calling function.
The common practice we are trying to override here is what standard stack unwinding throughout many languages suggests in an exception, (and hence the word “try” was selected…). But if we could only allow a function(…try() or other) that would jump back two levels in the trace, then
try := handler(err error) { //which corelates with - try := func(err error)
if err !=nil{
//do what ever you want to do when there's an error... log/print etc
return2 //2 levels
}
}
and then a code like f := try(os.Open(filename)) could do exactly as the proposal advises, but as its a function (or actually a “handler function”) the developer will have much more control on what the function does, how it formats the error in different cases, use a similar handler all around the code to handle (lets say) os.Open, instead of wrting fmt.Errorf(“error opening file %s ….”) every time. This would also force error handling as if “try” would not be defined - its a compile time error.
@guybrand Having such a two-level return return2
(or “non-local return” as the general concept is called in Smalltalk) would be a nice general-purpose mechanism (also suggested by @mikeschinkel in #32473). But it appears that try
is still needed in your suggestion, so I don’t see a reason for the return2
- the try
can just do the return
. It would be more interesting if one could also write try
locally, but that’s not possible for arbitrary signatures.
@griesemer
> “so I don’t see a reason for the return2
- the try
can just do the return
.”
One reason — as I pointed out in #32473 (thanks for the reference) — would be to allow multiple levels of break
and continue
, in addition to return
.
@griesemer
Thanks for the ref to @mikeschinkel #32473, it does have a lot in common.
regarding > But it appears that try is still needed in your suggestion Although my suggestion can be implemented with “any” handler and not a reserved “build in/keyword/expression” I do not think “try()” is a bad idea (and therefore did not down vote it), I’m trying to “broaden it up” - so it would show more upsides, many expected “once go 2.0 is introduced”
I think that may also be the source of the “mixed vibes” you reported on your last summary - its not “try() does not improve error handling” - sure it does, its “waiting for Go 3.0 to solve some other major error handling pains” people stated above, looks like too long :)
I am conducting a survey on “error handling pains” (and sound out some of the pains are merely “I dont use good practices”, whereas some I did not even imagine people (mostly coming from other languages) want to do - from cool to WTF).
Hope I can share some interesting results soon.
lastly -Thanks for the amazing work and patience !
Here’s a modification that may help with some of the concerns raised: Treat try
like a goto
instead of like a return
. Hear me out. :)
try
would instead be syntactic sugar for:
t1, … tn, te := f() // t1, … tn, te are local (invisible) temporaries
if te != nil {
err = te // assign te to the error result parameter
goto error // goto "error" label
}
x1, … xn = t1, … tn // assignment only if there was no error
Benefits:
defer
is not required to decorate errors. (Named returns are still required, though.)error:
label is a visual clue that there is a try
somewhere in the function.This also provides a mechanism for adding handlers that sidesteps the handler-as-function problems: Use labels as handlers. try(fn(), wrap)
would goto wrap
instead of goto error
. The compiler can confirm that wrap:
is present in the function. Note that having handlers also helps with debugging: You can add/alter the handler to provide a debugging path.
Sample code:
func CopyFile(src, dst string) (err error) {
r := try(os.Open(src))
defer r.Close()
w := try(os.Create(dst))
defer func() {
w.Close()
if err != nil {
os.Remove(dst) // only if a “try” fails
}
}()
try(io.Copy(w, r), copyfail)
try(w.Close())
return nil
error:
return fmt.Errorf("copy %s %s: %v", src, dst, err)
copyfail:
recordMetric("copy failure") // count incidents of this failure
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
Other comments:
try
be preceded by a terminating statement. In practice, this would force them to the end of the function and could prevent some spaghetti code. On the other hand, it might prevent some reasonable, helpful uses.try
could be used to create a loop. I think this falls under the banner of “if it hurts, don’t do it”, but I’m not sure.Credit: I believe a variant of this idea was first suggested by @griesemer in person at GopherCon last year.
@josharian Yes, this is more or less exactly the version we discussed last year (except that we used check
instead of try
). I think it would be crucial that one couldn’t jump “back” into the rest of the function body, once we are at the error
label. That would ensure that the goto
is somewhat “structured” (no spaghetti code possible). One concern that was brought up was that the error handler (the error:
) label would always end up at the end of the function (otherwise one would have to jump around it somehow). Personally, I like the error handling code out of the way (at the end), but others felt that it should be visible right at the start.
You could avoid the goto labels forcing handlers to the end of the function by resurrecting the handle
/check
proposal in a simplified form. What if we used the handle err { ... }
syntax but just didn’t let handlers chain, instead only last one is used. It simplifies that proposal a lot, and is very similar with the goto idea, except it puts the handling closer to point of use.
func CopyFile(src, dst string) (err 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)
defer func() {
w.Close()
if err != nil {
os.Remove(dst) // only if a “check” fails
}
}()
{
// handlers are scoped, after this scope the original handle is used again.
// as an alternative, we could have repeated the first handle after the io.Copy,
// or come up with a syntax to name the handlers, though that's often not useful.
handle err {
recordMetric("copy failure") // count incidents of this failure
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
check io.Copy(w, r)
}
check w.Close()
return nil
}
As a bonus, this has a future path to letting handlers chain, as all existing uses would have a return.
@josharian @griesemer if you introduce named handlers (which many responses to check/handle requested, see recurring themes), there are syntax options preferable to try(f(), err)
:
try.err f()
try?err f()
try#err f()
?err f() // because 'try' is redundant
?return f() // no handler
?panic f() // no handler
(?err f()).method()
f?err() // lead with function name, instead of error handling
f?err().method()
file, ?err := os.Open(...) // many check/handle responses also requested this style
One of the things I like most about Go is that its syntax is relatively punctuation-free, and can be read out loud without major problems. I would really hate for Go to end up as a $#@!perl
.
…
This is all preface to a few ideas I wanted to throw into the mix which would benefit from try
as a keyword.
I’d propose the following constructions.
1) No handler
// The existing proposal, but as a keyword rather than builtin. When an error is
// "caught", the function returns all zero values plus the error. Nothing
// particularly new here.
func doSomething() (int, error) {
try SomeFunc()
a, b := try AnotherFunc()
// ...
return 123, nil
}
2) Handler
Note that error handlers are simple code blocks, intended to be inlined, rather than functions. More on this below.
func doSomething() (int, error) {
// Inline error handler
a, b := try SomeFunc() else err {
return 0, errors.Wrap(err, "error in doSomething:")
}
// Named error handlers
handler logAndContinue err {
log.Errorf("non-critical error: %v", err)
}
handler annotateAndReturn err {
return 0, errors.Wrap(err, "error in doSomething:")
}
c, d := try SomeFunc() else logAndContinue
e, f := try OtherFunc() else annotateAndReturn
// ...
return 123, nil
}
Proposed restrictions:
- You can only try
a function call. No try err
.
- If you do not specify a handler, you can only try
from within a function that returns an error as its rightmost return value. There’s no change in how try
behaves based on its context. It never panics (as discussed much earlier in the thread).
- There is no “handler chain” of any kind. Handlers are just inlineable code blocks.
Benefits:
- The try
/else
syntax could be trivially desugared into the existing “compound if”:
go
a, b := try SomeFunc() else err {
return 0, errors.Wrap(err, "error in doSomething:")
}
becomes
go
if a, b, err := SomeFunc(); err != nil {
return 0, errors.Wrap(err, "error in doSomething:")
}
To my eye, compound ifs have always seemed more confusing than helpful for a very simple reason: conditionals generally occur after an operation, and have something to do with processing its results. If the operation is wedged inside of the conditional statement, it’s simply less obvious that it’s happening. The eye is distracted. Furthermore, the scope of the defined variables is not as immediately obvious as when they’re leftmost on a line.
- Error handlers are intentionally not defined as functions (nor with anything resembling function-like semantics). This does several things for us:
- The compiler can simply inline a named handler wherever it’s referred to. It’s much more like a simple macro/codegen template than a function call. The runtime doesn’t even need to know that handlers exist.
- We’re not limited regarding what we can do inside of a handler. We get around the criticism of check
/handle
that “this error handling framework is only good for bailouts”. We also get around the “handler chain” criticism. Any arbitrary code can be placed inside one of these handlers, and no other control flow is implied.
- We don’t have to hijack return
inside the handler to mean super return
. Hijacking a keyword is extremely confusing. return
just means return
, and there’s no real need for super return
.
- defer
doesn’t need to moonlight as an error handling mechanism. We can continue to think of it mainly as a way to clean up resources, etc.
- Regarding adding context to errors:
- Adding context with handlers is extremely simple and looks very similar to existing if err != nil
blocks
- Even though the “try without handler” construct doesn’t directly encourage adding context, it’s very straightforward to refactor into the handler form. Its intended use would primarily be during development, and it would be extremely straightforward to write a go vet
check to highlight unhandled errors.
Apologies if these ideas are very similar to other proposals — I’ve tried to keep up with them all, but may have missed a good deal.
…
Regarding your proposal: It would stand just fine even without named handlers, wouldn’t it? (That would simplify the proposal without loss of power. One could simply call a local function from the inlined handler.)
Regarding your proposal: It would stand just fine even without named handlers, wouldn’t it? (That would simplify the proposal without loss of power. One could simply call a local function from the inlined handler.)
@griesemer Indeed — I was feeling pretty lukewarm about including those. Certainly more Go-ish without.
On the other hand, it does seem that people want the ability to do one-liner error handling, including one-liners that return
. A typical case would be log, then return
. If we shell out to a local function in the else
clause, we probably lose that:
a, b := try SomeFunc() else err {
someLocalFunc(err)
return 0, err
}
(I still prefer this to compound ifs, though)
However, you could still get one-liner returns that add error context by implementing a simple gofmt
tweak discussed earlier in the thread:
a, b := try SomeFunc() else err { return 0, errors.Wrap(err, "bad stuff!") }
@griesemer if handlers are back on the table, I suggest you make a new issue for discussion of try/handle or try/label. This proposal specifically omitted handlers, and there are innumerable ways to define and invoke them.
Anyone suggesting handlers should first read the check/handle feedback wiki. Chances are good that whatever you dream up is already described there :-) https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback
Is the new keyword necessary in the above proposal? Why not:
SomeFunc() else return
a, b := SomeOtherFunc() else err { return 0, errors.Wrap(err, "bad stuff!") }
@smonkewitz no, a new keyword isn’t necessary in that version as its bound to assignment statements, which has been mentioned multiple times thus far in various syntax sugar.
https://github.com/golang/go/issues/32437#issuecomment-499808741 https://github.com/golang/go/issues/32437#issuecomment-499852124 https://github.com/golang/go/issues/32437#issuecomment-500095505
@ianlancetaylor has this particular flavor of error handling been considered by the go team yet? Its not as easy to implement as the proposed try builtin but feels more idiomatic. ~unnecessary statement, sorry.~
I’m happy to lob my support behind the last few posts where
try
(the keyword) has been moved to the beginning of the line. It really ought to share the same visual space asreturn
.
Given the two options for this point and assuming that one was chosen and thus set somewhat of a precedent for future potential features:
A.)
try f := os.Open(filepath) else err {
return errors.Wrap(err, "can't open")
}
B.)
f := try os.Open(filepath) else err {
return errors.Wrap(err, "can't open")
}
Which of the two provide more flexibility for future new keyword use? (I do not know the answer to this as I have not mastered the dark art of writing compilers.) Would one approach be more limiting than another?
@james-lawrence In reply to https://github.com/golang/go/issues/32437#issuecomment-500116099 : I don’t recall ideas like an optional , err
being seriously considered, no. Personally I think it’s a bad idea, because it means that if a function changes to add a trailing error
parameter, existing code will continue to compile, but will act very differently.
@ianlancetaylor
” I think it’s a bad idea, because it means that if a function changes to add a trailing
error
parameter, existing code will continue to compile, but will act very differently.”
That is probably the correct way to look at it, if you are able to control both the upstream and downstream code so that if you need to change a function signature in order to also return an error then you can do so.
But I would ask you to consider what happens when someone does not control either upstream or downstream of their own packages? And also to consider the use-cases where errors might be added, and what happens if errors need to be added but you cannot force downstream code to change?
Can you think of an example where someone would change signature to add a return value? For me they have typically fallen into the category of “I did not realize an error would occur” or “I’m feeling lazy and don’t want to go to the effort because the error probably won’t happen.”
In both of those cases I might add an error return because it becomes apparent an error needs to be handled. When that happens, if I cannot change the signature because I don’t want to break compatibility for other developers using my packages, what to do? My guess is that the vast majority of the time the error will occur and that the code that called the func that does not return the error will act very differently, anyway.
Actually, I rarely do the latter but too frequently do the former. But I have noticed 3rd party packages frequently ignore capturing errors where they should be, and I know this because when I bring up their code in GoLand flags in bright orange every instance. I would love to be able to submit pull requests to add error handling to the packages I use a lot, but if I do most won’t accept them because I would be breaking their code signatures.
By not offering a backward compatible way to add errors to be returned by functions, developers who distribute code and care about not breaking things for their users won’t be able to evolve their packages to include the error handling as they should.
Maybe rather than consider the problem being that code will act different instead view the problem as an engineering challenge regarding how to minimize the downside of a method that is not actively capturing an error? That would have broader and longer term value.
For example, consider adding a package error handler that one must set before being able to ignore errors? Or require a local handler in a function before allowing it?
To be frank, Go’s idiom of returning errors in addition to regular return values was one of its better innovations. But as so often happens when you improve things you often expose other weaknesses and I will argue that Go’s error handling did not innovate enough.
We Gophers have become steeped in returning an error rather than throwing an exception so the question I have is “Why shouldn’t we been returning errors from every function?” We don’t always do so because writing code without error handling is more convenient than coding with it. So we omit error handling when we think we can get away from it. But frequently we guess wrong.
So really, if it were possible to figure out how to make the code elegant and readable I would argue that return values and errors really should be handled separately, and that every function should have the ability to return errors regardless of its past function signatures. And getting existing code to gracefully handle code that now generates errors would be a worthwhile endeavor.
I have not proposed anything because I have not been able to envision a workable syntax, but if we want to be honest with ourselves, hasn’t everything in this thread and related to Go’s error handling in general been about the fact that error handling and program logic are strange bedfellows so ideally errors would be best handled out-of-band in some way?
A lot of ways of doing handlers are being proposed, but I think they often miss two key requirements:
It has to be significantly different and better than if x, err := thingie(); err != nil { handle(err) }
. I think suggestions along the lines of try x := thingie else err { handle(err) }
don’t meet that bar. Why not just say if
?
It should be orthogonal to the existing functionality of defer
. That is, it should be different enough that it is clear that the proposed handling mechanism is needed in its own right without creating weird corner cases when handle and defer interact.
Please keep these desiderata in mind as we discuss alternative mechanisms for try
/handle.
Regarding try else
, I think conditional error functions like fmt.HandleErrorf
(edit: I’m assuming it returns nil when the input is nil) in the initial comment work fine so adding else
is unnecessary.
a, b, err := doSomething()
try fmt.HandleErrorf(&err, "Decorated "...)
Like many others here, I prefer try
to be a statement rather than an expression, mostly because an expression altering control flow is completely alien to Go. Also because this is not an expression, it should be at the beginning of the line.
I also agree with @daved that the name is not appropriate. After all, what we’re trying to achieve here is a guarded assignment, so why not use guard
like in Swift and make the else
clause optional? Something like
GuardStmt = "guard" ( Assignment | ShortVarDecl | Expression ) [ "else" Identifier Block ] .
where Identifier
is the error variable name bound in the following Block
. With no else
clause, just return from the current function (and use a defer handler to decorate errors if need be).
I initially didn’t like an else
clause because it’s just syntactic sugar around the usual assignment followed by if err != nil
, but after seing some of the examples, it just makes sense: using guard
makes the intent clearer.
EDIT: some suggested to use things like catch
to somehow specify different error handlers. I find else
equally viable semantically speaking and it’s already in the language.
While I like the try-else statement, how about this syntax?
a, b, (err) := func() else { return err }
Expression try
-else
is a ternary operator.
a, b := try f() else err { ... }
fmt.Println(try g() else err { ... })`
Statement try
-else
is an if
statement.
try a, b := f() else err { ... }
// (modulo scope of err) same as
a, b, err := f()
if err != nil { ... }
Builtin try
with an optional handler can be achieved with either a helper function (below) or not using try
(not pictured, we all know what that looks like).
a, b := try(f(), decorate)
// same as
a, b := try(g())
// where g is
func g() (whatever, error) {
x, err := f()
if err != nil {
try(decorate(err))
}
return x, nil
}
All three cut down on the boilerplate and help contain the scope of errors.
It gives the most savings for builtin try
but that has the issues mentioned in the design doc.
For statement try
-else
, it provides an advantage over using if
instead of try
. But the advantage is so marginal that I have a hard time seeing it justify itself, though I do like it.
All three assume that it is common to need special error handling for individual errors.
Handling all errors equally can be done in defer
. If the same error handling is being done in each else
block that’s a bit repetitious:
func printSum(a, b string) error {
try x := strconv.Atoi(a) else err {
return fmt.Errorf("printSum: %w", err)
}
try y := strconv.Atoi(b) else err {
return fmt.Errorf("printSum: %w", err)
}
fmt.Println("result:", x + y)
return nil
}
I certainly know that there are times when a certain error requires special handling. Those are the instances that stick out in my memory. But, if that only happens, say, 1 out of 100 times, wouldn’t it be better to keep try
simple and just not use try
in those situations? On the other hand, if it’s more like 1 out of 10 times, adding else
/handler seems more reasonable.
It would be interesting to see an actual distribution of how often try
without an else
/handler vs try
with an else
/handler would be useful, though that’s not easy data to gather.
I want to expand on @jimmyfrasche ’s recent comment.
The goal of this proposal is to reduce the boilerplate
a, b, err := f()
if err != nil {
return nil, err
}
This code is easy to read. It’s only worth extending the language if we can achieve a considerable reduction in boilerplate. When I see something like
try a, b := f() else err { return nil, err }
I can’t help but feel that we aren’t saving that much. We’re saving three lines, which is good, but by my count we’re cutting back from 56 to 46 characters. That’s not much. Compare to
a, b := try(f())
which cuts from 56 to 18 characters, a much more significant reduction. And while the try
statement makes the potential change of flow of control more clear, overall I don’t find the statement more readable. Though on the plus side the try
statement makes it easier to annotate the error.
Anyhow, my point is: if we’re going to change something, it should significantly reduce boilerplate or should be significantly more readable. The latter is pretty hard, so any change needs to really work on the former. If we get only a minor reduction in boilerplate, then in my opinion it’s not worth doing.
@ianlancetaylor
If I understand “try else” proposal correctly, it seems that else
block is optional, and reserved for user provided handling. In your example try a, b := f() else err { return nil, err }
the else
clause is actually redundant, and the whole expression can be written simply as try a, b := f()
I agree with @ianlancetaylor , Readability and boilerplate are two main concerns and perhaps the drive to the go 2.0 error handling (though I can add some other important concerns)
Also, that the current
a, b, err := f()
if err != nil {
return nil, err
}
Is highly readable. And since I believe
if a, b, err := f(); err != nil {
return nil, err
}
Is almost as readable, yet had it’s scope “issues”, perhaps a
ifErr a, b, err := f() {
return nil, err
}
That would only the ; err != nil part, and would not create a scope, or
similarly
try a, b, err := f() { return nil, err }
Keeps the extra two lines, but is still readable.
@ianlancetaylor > Anyhow, my point is: if we’re going to change something, it should significantly reduce boilerplate or should be significantly more readable. The latter is pretty hard, so any change needs to really work on the former. If we get only a minor reduction in boilerplate, then in my opinion it’s not worth doing.
Agreed, and considering that an else
would only be syntactic sugar (with a weird syntax!), very likely used only rarely, I don’t care much about it. I’d still prefer try
to be a statement though.
@ianlancetaylor Echoing @DmitriyMV, the else
block would be optional. Let me throw in an example that illustrates both (and doesn’t seem too far off the mark in terms of the relative proportion of handled vs. non-handled try
blocks in real code):
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
headRef, err := r.Head()
if err != nil {
return err
}
parentObjOne, err := headRef.Peel(git.ObjectCommit)
if err != nil {
return err
}
parentObjTwo, err := remoteBranch.Reference.Peel(git.ObjectCommit)
if err != nil {
return err
}
parentCommitOne, err := parentObjOne.AsCommit()
if err != nil {
return err
}
parentCommitTwo, err := parentObjTwo.AsCommit()
if err != nil {
return err
}
treeOid, err := index.WriteTree()
if err != nil {
return err
}
tree, err := r.LookupTree(treeOid)
if err != nil {
return err
}
remoteBranchName, err := remoteBranch.Name()
if err != nil {
return err
}
userName, userEmail, err := r.UserIdentityFromConfig()
if err != nil {
userName = ""
userEmail = ""
}
var (
now = time.Now()
author = &git.Signature{Name: userName, Email: userEmail, When: now}
committer = &git.Signature{Name: userName, Email: userEmail, When: now}
message = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
parents = []*git.Commit{
parentCommitOne,
parentCommitTwo,
}
)
_, err = r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
if err != nil {
return err
}
return nil
}
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
try parentCommitOne := parentObjOne.AsCommit()
try parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)
try remoteBranchName := remoteBranch.Name()
try userName, userEmail := r.UserIdentityFromConfig() else err {
userName = ""
userEmail = ""
}
var (
now = time.Now()
author = &git.Signature{Name: userName, Email: userEmail, When: now}
committer = &git.Signature{Name: userName, Email: userEmail, When: now}
message = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
parents = []*git.Commit{
parentCommitOne,
parentCommitTwo,
}
)
try r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
return nil
}
While the try
/else
pattern doesn’t save many characters over compound if
, it does:
- unify the error handling syntax with the unhandled try
- make it clear at a glance that a conditional block is handling an error condition
- give us a chance to reduce scoping weirdness that compound if
s suffer from
Unhandled try
will likely be the most common, though.
@brynbellomy
discarding the scope issue (which may be resolved in other ways), I’m not sure
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
if headRef, err := r.Head(); err != nil {
return err
} else if parentObjOne, err := headRef.Peel(git.ObjectCommit); err != nil {
return err
} else parentObjTwo, err := remoteBranch.Reference.Peel(git.ObjectCommit); err != nil {
return err
} ...
is not so different readability wise, yet (or fmt.Errorf(“error with getting head : %s”, err.Error() ) allows you to modify and give extra data easily.
What’s still a nag is the 1. having to recheck ; err != nil 2. returning the error as is if we do not want to give the extra info - which in some cases is not a good practice, cause you are depending on the function you call to reflect a “good” error that will hint on “what went wrong”, in cases of file.Open , close , Remove , Db functions etc many of the function calls can return the same error (we can argue if that means the developer that wrote the error did a good job or not… but it DOES happen) - and then - you have an error, probably log it from the function that called “ createMergeCommit”, but cant trace it to the exact line on which it occurred.
One alternative to @brynbellomy’s else
suggestion of:
a, b := try f() else err { /* handle error */ }
could be to support a decoration function immediately after the else:
decorate := func(err error) error { return fmt.Errorf("foo failed: %v", err) }
try a, b := f() else decorate
try c, d := g() else decorate
And maybe also some utility functions something along the lines of:
decorate := fmt.DecorateErrorf("foo failed")
The decoration function could have signature func(error) error
, and be called by try in the presence of an error, just before try returns from the associated function being tried.
That would be similar in spirit to one of the earlier “design iterations” from the proposal document:
f := try(os.Open(filename), handler) // handler will be called in error case
If someone wants something more complex or a block of statements, they could instead use if
(just as they can today).
That said, there is something nice about the visual alignment of try
shown in @brynbellomy’s example in https://github.com/golang/go/issues/32437#issuecomment-500949780.
All of this could still work with defer
if that approach gets chosen for uniform error decoration (or even in theory there could be an alternative form of registering a decoration function, but that is a separate point).
In any event, I’m not sure what is best here, but wanted to make another option explicit.
Here’s @brynbellomy’s example rewritten with the try
function, using a var
block to retain the nice alignment that @thepudds pointed out in https://github.com/golang/go/issues/32437#issuecomment-500998690.
package main
import (
"fmt"
"time"
)
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
var (
headRef = try(r.Head())
parentObjOne = try(headRef.Peel(git.ObjectCommit))
parentObjTwo = try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne = try(parentObjOne.AsCommit())
parentCommitTwo = try(parentObjTwo.AsCommit())
treeOid = try(index.WriteTree())
tree = try(r.LookupTree(treeOid))
remoteBranchName = try(remoteBranch.Name())
)
userName, userEmail, err := r.UserIdentityFromConfig()
if err != nil {
userName = ""
userEmail = ""
}
var (
now = time.Now()
author = &git.Signature{Name: userName, Email: userEmail, When: now}
committer = &git.Signature{Name: userName, Email: userEmail, When: now}
message = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
parents = []*git.Commit{
parentCommitOne,
parentCommitTwo,
}
)
_, err = r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
return err
}
It’s as succinct as the try
-statement version, and I would argue just as readable. Since try
is an expression, a few of those intermediate variables could be eliminated, at the cost of some readability, but that seems more like a matter of style than anything else.
It does raise the question of how try
works in a var
block, though. I assume each line of the var
counts as a separate statement, rather than the whole block being a single statement, as far as the order of what gets assigned when.
@thepudds
try a, b := f() else decorate
Perhaps its too deep a burn in my brain cells, but this hits me too much as a
try a, b := f() ;catch(decorate)
and a slippery slope to a
try
a, b := f()
catch(decorate)
I think you can see where that’s leading, and for me comparing
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
try parentCommitOne := parentObjOne.AsCommit()
try parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)
try remoteBranchName := remoteBranch.Name()
with
try (
headRef := r.Head()
parentObjOne := headRef.Peel(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()
tree := r.LookupTree(treeOid)
remoteBranchName := remoteBranch.Name()
)
(or even a catch at the end) The second is more readable, but emphasizes the fact the functions below return 2 vars, and we magically discard one, collecting it into a “magic returned err” .
try(err) (
headRef := r.Head()
parentObjOne := headRef.Peel(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()
tree := r.LookupTree(treeOid)
remoteBranchName := remoteBranch.Name()
); err!=nil {
//handle the err
}
at least explicitly sets the variable to return, and let me handle it within the function, whenever I want.
I think the optional else
in try ... else { ... }
will push code too much to the right, possibly obscuring it. I expect the error block should take at least 25 chars most of the time. Also, up until now blocks are not kept on the same line by go fmt
and I expect this behavior will be kept for try else
. So we should be discussing and comparing samples where the else
block is on a separate line. But even then I am not sure about the readability of else {
at the end of the line.
Sorry if someone already posted something like this (there are a lot of good ideas :P ) How about this alternative syntax:
fail := func(err error) error {
log.Print("unexpected error", err)
return err
}
a, b, err := f1() // normal
c, d := f2() -> throw // calls throw(err)
e, f := f3() -> panic // calls panic(err)
g, h := f4() -> t.Error // calls t.Error(err)
i, j := f5() -> fail // calls fail(err)
that is, you have a -> handler
on the right of a function call which is called if the returned err != nil. The handler is any function which accepts an error as a single argument and optionally returns an error (i.e., func(error)
or func(error) error
). If the handler returns a nil error the function continues otherwise the error is returned.
so a := b() -> handler
is equivalent to:
a, err := b()
if err != nil {
if herr := handler(err); herr != nil {
return herr
}
}
Now, as a shortcut you could support a try
builtin (or keyword or ?=
operator or whatever) which is short for a := b() -> throw
so you could write something like:
func() error {
a, b := try(f1())
c, d := try(f2())
e, f := try(f3())
...
return nil
}() -> panic // or throw/fail/whatever
Personally I find a ?=
operator easier to read than a try keyword/builtin:
func() error {
a, b ?= f1()
c, d ?= f2()
e, f ?= f3()
...
return nil
}() -> panic
note: here I’m using throw as a placeholder for a builtin that would return the error to the caller.
I’d like to join the fray to mention another two possibilities, each of which are independent, so I’ll keep them in separate posts.
I thought the suggestion that try()
(with no arguments) could be defined to return a pointer to the error return variable was an interesting one, but I wasn’t keen on that kind of punning - it smacks of function overloading, something that Go avoids.
However I liked the general idea of a predefined identifier that refers to the local error value.
So, how about predefining the err
identifier itself to be an alias for the error return variable? So this would be valid:
func foo() error {
defer handleError(&err, etc)
try(something())
}
It would be functionally identical to:
func foo() (err error) {
defer handleError(&err, etc)
try(something())
}
The err
identifier would be defined at universe scope, even though it acts as a function-local alias, so any package-level definition or function-local definition of err
would override it. This might seem dangerous but I scanned the 22m lines of Go in the Go corpus and it’s very rare. There are only 4 distinct instances err
used as a global (all as a variable, not a type or constant) - this is something that vet
could warn about.
It’s possible that there may be two function error return variables in scope; in this case, I think it’s best that the compiler would complain that there’s an ambiguity and require the user to explicitly name the correct return variable. So this would be invalid:
func foo() error {
f := func() error {
defer handleError(&err, etc)
try(something())
return nil
}
return f()
}
but you could always write this instead:
func foo() error {
f := func() (err error) {
defer handleError(&err, etc)
try(something())
return nil
}
return f()
}
A small thing, but if try
is a keyword it could be recognized as a terminating statement so instead of
func f() error {
try(g())
return nil
}
you can just do
func f() error {
try g()
}
(try
-statement gets that for free, try
-operator would need special handling, I realize the above is not a great example: but it is minimal)
@jimmyfrasche try
could be recognized as a terminating statement even if it is not a keyword - we already do that with panic
, there’s no extra special handling needed besides what we already do. But besides that point, try
is not a terminating statement, and trying to make it one artificially seems odd.
All valid points. I guess it could only reliably be considered as a terminating statement if it’s the very last line of a function that only returns an error, like CopyFile
in the detailed proposal, or it’s being used as try(err)
in an if
where it’s known that err != nil
. Doesn’t seem worth it.
Given the discussion thus far — specifically the responses from the Go team — I am getting the strong impression the team plans to move forward with the proposal that is on the table. If yes, then a comment and a request:
The as-is proposal IMO will result in a non-insignificant reduction in code quality in the publicly available repos. My expectation is many developers will take the path of least resistance, effectively use exception handling techniques and choose to use try()
instead of handling errors at the point they occur. But given the prevailing sentiment on this thread I realize that any grandstanding now would just be fighting a losing battle so I am just registering my objection for posterity.
Assuming that the team does move forward with the proposal as currently written, can you please add a compiler switch that will disable try()
for those who do not want any code that ignores errors in this manner and to disallow programmers they hire from using it? (via CI, of course.) Thank you in advance for this consideration.
can you please add a compiler switch that will disable try()
This would have to be on a linting tool, not on the compiler IMO, but I agree
This would have to be on a linting tool, not on the compiler IMO, but I agree
I am explicitly requesting a compiler option and not a linting tool because to disallow compiling such as option. Otherwise it will be too easy to “forget” to lint during local development.
@mikeschinkel Wouldn’t it be just as easy to forget to turn on the compiler option in that situation?
Compiler flags should not change the spec of the language. This is much more fit for vet/lint
Wouldn’t it be just as easy to forget to turn on the compiler option in that situation?
Not when using tools like GoLand where there is no way to force a lint to be run before a compile.
Compiler flags should not change the spec of the language.
-nolocalimports
changes the spec, and -s
warns.
Compiler flags should not change the spec of the language.
-nolocalimports
changes the spec, and-s
warns.
No, it doesn’t change the spec. Not only does the grammar of the language continue to remain the same, but the spec specifically states: > The interpretation of the ImportPath is implementation-dependent but it is typically a substring of the full file name of the compiled package and may be relative to a repository of installed packages.
Not when using tools like GoLand where there is no way to force a lint to be run before a compile.
https://github.com/vmware/dispatch/wiki/Configure-GoLand-with-golint
@deanveloper
https://github.com/vmware/dispatch/wiki/Configure-GoLand-with-golint
Certainly that exists, but you are comparing apple-to-organges. What you are showing is a file watcher that runs on files changing and since GoLand autosaves files that means it runs constantly which generates far more noise than signal.
The lint always does not and cannot (AFAIK) be configured to as a pre-condition for running the compiler:
No, it doesn’t change the spec. Not only does the grammar of the language continue to remain the same, but the spec specifically states:
You are playing with semantics here instead of focusing on the outcome. So I will do the same.
I request that a compiler option be added that will disallow compiling code with try()
. That is not a request to change the language spec, it is just a request to for the compiler to halt in this special case.
And if it helps, the language spec can be updated to say something like:
The interpretation of
try()
is implementation-dependent but it is typically a one that triggers a return when the last parameter is an error however it can be implemented to not be allowed.
The time to ask for a compiler switch or vet check is after the try()
prototype lands in 1.14(?) tip. At that point you’d file a new issue for it (and yes I think it’s a good idea). We’ve been asked to restrict comments here to factual input about the current design doc.
This is a response to @mikeschinkel, I’m putting my response in a detail block so that I don’t clutter up the discussion too much. Either way, @networkimprov is correct that this discussion should be tabled until after this proposal gets implemented (if it does).
details about a flag to disable try
@mikeschinkel
The lint always does not and cannot (AFAIK) be configured to as a pre-condition for running the compiler:
Reinstalled GoLand just to test this. This seems to work just fine, the only difference being is that if the lint finds something it doesn’t like, it doesn’t fail the compilation. That could easily be fixed with a custom script though, that runs golint
and fails with a nonzero exit code if there is any output.
(Edit: I fixed the error that it was trying to tell me at the bottom. It was running fine even while the error was present, but changing “Run Kind” to directory removed the error and it worked fine)
Also another reason why it should NOT be a compiler flag - all Go code is compiled from source. That includes libraries. That means that if you want to turn of try
through the compiler, you’d be turning off try
for every single one of the libraries you are using as well. It’s just a bad idea to have it be a compiler flag.
You are playing with semantics here instead of focusing on the outcome.
No, I am not. Compiler flags should not change the spec of the language. The spec is very well layed-out and in order for something to be “Go”, it needs to follow the spec. The compiler flags you have mentioned do change the behavior of the language, but no matter what, they make sure the language still follows the spec. This is an important aspect of Go. As long as you follow the Go spec, your code should compile on any Go compiler.
I request that a compiler option be added that will disallow compiling code with try(). That is not a request to change the language spec, it is just a request to for the compiler to halt in this special case.
It is a request to change the spec. This proposal in itself is a request to change the spec. Builtin functions are very specifically included in the spec.. Asking to have a compiler flag that removes the try
builtin would therefore be a compiler flag that would change the spec of the language being compiled.
That being said, I think that ImportPath
should be standardized in the spec. I may make a proposal for this.
And if it helps, the language spec can be updated to say something like […]
While this is true, you would not want the implementation of try
to be implementation dependent. It’s made to be an important part of the language’s error handling, which is something that would need to be the same across every Go compiler.
@deanveloper > “Either way, @networkimprov is correct that this discussion should be tabled until after this proposal gets implemented (if it does).”
Then why did you decide to ignore that suggestion and post in this thread anyway instead of waiting for later? You argued your points here while at the same time asserting that I should not challenge your points. Practice what you preach…
Given you choice, I will choose to respond too, also in a detail block here:
“That could easily be fixed with a custom script though, that runs golint and fails with a nonzero exit code if there is any output.”
Yes, with enough coding any problem can be fixed. But we both know from experience that the more complex a solution is the fewer people who want to use it will actually end up using it.
So I was explicitly asking for a simple solution here, not a roll-your-own solution.
“you’d be turning off try for every single one of the libraries you are using as well.”
And that is explicitly the reason why I requested it. Because I want to ensure that all code that uses this troublesome “feature” will not make its way into executables we distribute.
“It is a request to change the spec. This proposal in itself is a request to change the spec.“
It is ABSOLUTELY not a change to the spec. It is a request for a switch to change the behavior of the build
command, not a change in language spec.
If someone asks for the go
command to have a switch to display its terminal output in Mandarin, that is not a change to the language spec.
Similarly if go build
were to sees this switch then it would simply issue an error message and halt when it comes across a try()
. No language spec changes needed.
“It’s made to be an important part of the language’s error handling, which is something that would need to be the same across every Go compiler.”
It will be a problematic part of the language’s error handling and making it optional will allow those who want to avoid its problems to be able to do so.
Without the switch it is likely most people will just see as a new feature and embrace it and never ask themselves if in fact it should be used.
With the switch — and articles explaining the new feature that mention the switch — many people will understand that it has problematic potential and thus will allow the Go team to study if it was a good inclusion or not by seeing how much public code avoids using it vs. how public code uses it. That could inform design of Go 3.
“No, I am not. Compiler flags should not change the spec of the language.”
Saying you are not playing semantics does not mean you are not playing semantics.
Fine. Then I instead request a new top level command called (something like) build-guard
used to disallow problematic features during compilation, starting with disallowing try()
.
Of course the best outcome is if the try()
feature is tabled with a plan to reconsider solving the issue a different way the future, a way in which the vast majority agrees with. But I fear the ship has already sailed on try()
so I am hoping to minimize its downside.
So now if you truly agree with @networkimprov then hold your reply until later, as they suggested.
I’m sorry, but there will not be compiler options to disable specific Go features, nor will there be vet checks saying not to use those features. If the feature is bad enough to disable or vet, we will not put it in. Conversely, if the feature is there, it is OK to use. There is one Go language, not a different language for each developer based on their choice of compiler flags.
…
This is a total repeat of what others have comments, but what basically providing try()
is analogous in many ways to simply embracing the following as idomatic code, and this is code that will never find its way into any code any self-respecting developer ships:
f, _ := os.Open(filename)
I know I can be better in my own code, but I also know many of us depend on the largess of other Go developers who publish some tremendously useful packages, but from what I have seen in “Other People’s Code™” best practices in error handling is often ignored.
So seriously, do we really want to make it easier for developers to ignore errors and allow them to polute GitHub with non-robust packages?
Given the discussion thus far — specifically the responses from the Go team — I am getting the strong impression the team plans to move forward with the proposal that is on the table. If yes, then a comment and a request:
The as-is proposal IMO will result in a non-insignificant reduction in code quality in the publicly available repos. My expectation is many developers will take the path of least resistance, effectively use exception handling techniques and choose to use try()
instead of handling errors at the point they occur. But given the prevailing sentiment on this thread I realize that any grandstanding now would just be fighting a losing battle so I am just registering my objection for posterity.
Assuming that the team does move forward with the proposal as currently written, can you please add a compiler switch that will disable try()
for those who do not want any code that ignores errors in this manner and to disallow programmers they hire from using it? (via CI, of course.) Thank you in advance for this consideration.
@mikeschinkel, twice now on this issue you have described the use of try as ignoring errors. On June 7 you wrote, under the heading “Makes it easier for developers to ignore errors”:
This is a total repeat of what others have comments, but what basically providing
try()
is analogous in many ways to simply embracing the following as idomatic code, and this is code that will never find its way into any code any self-respecting developer ships:f, _ := os.Open(filename)
I know I can be better in my own code, but I also know many of us depend on the largess of other Go developers who publish some tremendously useful packages, but from what I have seen in “Other People’s Code™” best practices in error handling is often ignored.
So seriously, do we really want to make it easier for developers to ignore errors and allow them to polute GitHub with non-robust packages?
And then on June 14 again you referred to using try as “code that ignores errors in this manner”.
If not for the code snippet f, _ := os.Open(filename)
, I would think you were simply exaggerating by characterizing “checking for an error and returning it” as “ignoring” an error. But the code snippet, along with the many questions already answered in the proposal document or in the language spec make me wonder whether we are talking about the same semantics after all. So just to be clear and answer your questions:
When studying the proposal’s code I find that the behaviour is non-obvious and somewhat hard to reason about.
When I see
try()
wrapping an expression, what will happen if an error is returned?
When you see try(f())
, if f()
returns an error, the try
will stop execution of the code and return that error from the function in whose body the try
appears.
Will the error just be ignored?
No. The error is never ignored. It is returned, the same as using a return statement. Like:
{ err := f(); if err != nil { return err } }
Or will it jump to the first or the most recent
defer
,
The semantics are the same as using a return statement.
Deferred functions run in “in the reverse order they were deferred.”
and if so will it automatically set a variable named
err
inside the closure that, or will it pass it as a parameter (I don’t see a parameter?).
The semantics are the same as using a return statement.
If you need to refer to a result parameter in a deferred function body, you can give it a name. See the result
example in https://golang.org/ref/spec#Defer_statements.
And if not an automatic error name, how do I name it? And does that mean I can’t declare my own
err
variable in my function, to avoid clashes?
The semantics are the same as using a return statement.
A return statement always assigns to the actual function results, even if the result is unnamed, and even if the result is named but shadowed.
And will it call all
defer
s? In reverse order or regular order?
The semantics are the same as using a return statement.
Deferred functions run in “in the reverse order they were deferred.” (Reverse order is regular order.)
Or will it return from both the closure and the
func
where the error was returned? (Something I would never have considered if I had not read here words that imply that.)
I don’t know what this means but probably the answer is no. I would encourage focusing on the proposal text and the spec and not on other commentary here about what that text might or might not mean.
After reading the proposal and all the comments thus far I still honestly do not know the answers to the above questions. Is that the kind of feature we want to add to a language whose advocates champion as being “Captain Obvious?”
In general we do aim for a simple, easy-to-understand language. I am sorry you had so many questions. But this proposal really is reusing as much of the existing language as possible (in particular, defers), so there should be very few additional details to learn. Once you know that
x, y := try(f())
means
tmp1, tmp2, tmpE := f()
if tmpE != nil {
return ..., tmpE
}
x, y := tmp1, tmp2
almost everything else should follow from the implications of that definition.
This is not “ignoring” errors. Ignoring an error is when you write:
c, _ := net.Dial("tcp", "127.0.0.1:1234")
io.Copy(os.Stdout, c)
and the code panics because net.Dial failed and the error was ignored, c is nil, and io.Copy’s call to c.Read faults. In contrast, this code checks and returns the error:
c := try(net.Dial("tcp", "127.0.0.1:1234"))
io.Copy(os.Stdout, c)
To answer your question about whether we want to encourage the latter over the former: yes.
@rsc
Thank you for your response.
“twice now on this issue you have described the use of try as ignoring errors.”
Yes, I was commenting using my perspective and not being technically correct.
What I meant was “Allowing errors to be passed on without being decorated.” To me that is “ignoring” — much like how people using exception handling “ignore” errors — but I can certainly see how others would view my wording as not being technically correct.
“When you see
try(f())
, iff()
returns an error, the try will stop execution of the code and return that error from the function in whose body the try appears.”
That was an answer to a question from my comment a while back, but by now I have figured that out.
And it ends up doing two things that make me sad. Reasons:
It will make the path of least resistance to avoid decorating errors — encouraging lots of developers to do just that — and many will publish that code for others to use resulting in more lower-quality publicly-available code with less robust error handling/error reporting.
For those like me who use break
and continue
for error handling instead of return
— a pattern that is more resilient to changing requirements — we won’t even be able to use try()
, even when there really is no reason to annotate the error.
“Or will it return from both the closure and the func where the error was returned? (Something I would never have considered if I had not read here words that imply that.)”
“I don’t know what this means but probably the answer is no. I would encourage focusing on the proposal text and the spec and not on other commentary here about what that text might or might not mean.”
Again, that question was over a week ago so I have a better understand now.
To clarify, for posterity, the defer
has a closure, right? If you return from that closure then — unless I misunderstand — it will not only return from the closure but also return from the func
where the error occurred, right? (No need to reply if yes.)
func example() {
defer func(err) {
return err // returns from both defer and example()
}
try(SomethingThatReturnsAnError)
}
BTW, my understanding is the reason for try()
is because developers have complained about boilerplate. I also find that sad because I think that the requirement to accept returned errors that results in this boilerplate is what helps make Go apps more robust than in many other languages.
I personally would prefer to see you make it harder to not decorate errors than to make it easier to ignore decorating them. But I do acknowledge that I appear to be in the minority on this.
BTW, some people have proposed syntax like one of the following (I have added a hypothetical .Extend()
to keep my examples concise):
f := try os.Open(filename) else err {
err.Extend("Cannot open file %s",filename)
//break, continue or return err
}
Or
try f := os.Open(filename) else err {
err.Extend("Cannot open file %s",filename)
//break, continue or return err
}
And then others claim that it does not really save any characters over this:
f,err := os.Open(filename)
if err != nil {
err.Extend("Cannot open file %s",filename)
//break, continue or return err
}
But one thing that criticism is missing that it moves from 5 lines to 4 lines, a reduction of vertical space and that seems significant, especially when you need many such constructs in a func
.
Even better would be something like this which would eliminate 40% of vertical space (though given the comments about keywords I doubt this would be considered):
try f := os.Open(filename)
else err().Extend("Cannot open file %s",filename)
end //break, continue or return err
#fwiw
ANYWAY, like I said earlier, I guess the ship has sailed so I will just learn to accept it.
@mikeschinkel,
To clarify, for posterity, the
defer
has a closure, right? If you return from that closure then — unless I misunderstand — it will not only return from the closure but also return from thefunc
where the error occurred, right? (No need to reply if yes.)
No. This is not about error handling but about deferred functions. They are not always closures. For example, a common pattern is:
func (d *Data) Op() int {
d.mu.Lock()
defer d.mu.Unlock()
... code to implement Op ...
}
Any return from d.Op runs the deferred unlock call after the return statement but before code transfers to the caller of d.Op. Nothing done inside d.mu.Unlock affects the return value of d.Op. A return statement in d.mu.Unlock returns from the Unlock. It does not by itself return from d.Op. Of course, once d.mu.Unlock returns, so does d.Op, but not directly because of d.mu.Unlock. It’s a subtle point but an important one.
Getting to your example:
> func example() { > defer func(err) { > return err // returns from both defer and example() > } > try(SomethingThatReturnsAnError) > } > ``` At least as written, this is an invalid program. I am not trying to be pedantic here - the details matter. Here is a valid program:
func example() (err error) { defer func() { if err != nil { println(“FAILED:“, err.Error()) } }()
try(funcReturningError())
return nil
} ```
Any result from a deferred function call is discarded when the call is executed, so in the case where what is deferred is a call to a closure, it makes no sense at all to write the closure to return a value. So if you were to write return err
inside the closure body, the compiler will tell you “too many arguments to return”.
So, no, writing return err
does not return from both the deferred function and the outer function in any real sense, and in conventional usage it’s not even possible to write code that appears to do that.
@rsc Thanks for clarifying.
Correct, I didn’t get the details right. I guess I don’t use defer
often enough to remember its syntax.
(FWIW I find using defer
for anything more complex than closing a file handle less obvious because of the jumping backwards in the func
before returning. So always just put that code at the end of the func
after the for range once{...}
my error handling code break
s out of.)
Sorry to interrupt, but I have facts to report :-)
I’m sure the Go team has already benchmarked defer, but I haven’t seen any numbers…
$ go test -bench=.
goos: linux
goarch: amd64
BenchmarkAlways2-2 20000000 72.3 ns/op
BenchmarkAlways4-2 20000000 68.1 ns/op
BenchmarkAlways6-2 20000000 68.0 ns/op
BenchmarkNever2-2 100000000 16.5 ns/op
BenchmarkNever4-2 100000000 13.1 ns/op
BenchmarkNever6-2 100000000 13.5 ns/op
Source
package deferbench
import (
"fmt"
"errors"
"testing"
)
func Always(iM, iN int) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("d: %v", err)
}
}()
if iN % iM == 0 {
return errors.New("e")
}
return nil
}
func Never(iM, iN int) (err error) {
if iN % iM == 0 {
return fmt.Errorf("r: %v", errors.New("e"))
}
return nil
}
func BenchmarkAlways2(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e2, a) }}
func BenchmarkAlways4(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e4, a) }}
func BenchmarkAlways6(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e6, a) }}
func BenchmarkNever2(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e2, a) }}
func BenchmarkNever4(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e4, a) }}
func BenchmarkNever6(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e6, a) }}
@networkimprov
From https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md#efficiency-of-defer (my emphasis in bold)
Independently, the Go runtime and compiler team has been discussing alternative implementation options and we believe that we can make typical defer uses for error handling about as efficient as existing “manual” code. We hope to make this faster defer implementation available in Go 1.14 (see also ** CL 171758 ** which is a first step in this direction).
i.e. defer is now 30% performance improvement for go1.13 for common usage, and should be faster and just as efficient as non-defer mode in go 1.14
Maybe someone can post numbers for 1.13 and the 1.14 CL?
Optimizations don’t always survive contact with the enemy… er, ecosystem.
1.13 defers will be about 30% faster:
name old time/op new time/op delta
Defer-4 52.2ns ± 5% 36.2ns ± 3% -30.70% (p=0.000 n=10+10)
This is what I get on @networkimprov ’s tests above (1.12.5 to tip):
name old time/op new time/op delta
Always2-4 59.8ns ± 1% 47.5ns ± 1% -20.57% (p=0.008 n=5+5)
Always4-4 57.9ns ± 2% 43.5ns ± 1% -24.96% (p=0.008 n=5+5)
Always6-4 57.6ns ± 2% 44.1ns ± 1% -23.43% (p=0.008 n=5+5)
Never2-4 13.7ns ± 8% 3.8ns ± 4% -72.27% (p=0.008 n=5+5)
Never4-4 10.5ns ± 6% 1.3ns ± 2% -87.76% (p=0.008 n=5+5)
Never6-4 10.8ns ± 6% 1.2ns ± 1% -88.46% (p=0.008 n=5+5)
(I’m not sure why Never ones are so much faster. Maybe inlining changes?)
The optimizations for defers for 1.14 are not implemented yet, so we don’t know what the performance will be. But we think we should get close to the performance of a regular function call.
…
Regarding the optimizations to defer, I’m excited for them. They help this proposal a little bit, making defer HandleErrorf(...)
a bit less heavy. I still don’t like the idea of abusing named parameters in order for this trick to work, though. How much is it expected to speed up by for 1.14? Should they run at similar speeds?
@randall77’s results for my benchmark show a 40+ns per call overhead for both 1.12 & tip. That implies that defer can inhibit optimizations, rendering improvements to defer moot in some cases.
@networkimprov Defer does currently inhibit optimizations, and that’s part of what we’d like to fix. For instance, it would be nice to inline the body of the defer’d function just like we inline regular calls.
I fail to see how any improvements we make would be moot. Where does that assertion come from?
Where does that assertion come from?
The 40+ns per call overhead for a function with a defer to wrap the error didn’t change.
The changes in 1.13 are one part of optimizing defer. There are other improvements planned. This is covered in the design document, and in the part of the design document quoted at some point above.
@griesemer One area that might be worth expanding a bit more is how transitions work in a world with try
, perhaps including:
vet
or staticcheck
or similar, vs. © might lead to a bug that might not be noticed or would need to be caught via testing.gopls
(or another utility) could or should have a role in automating common decoration style transitions.This is not exhaustive, but a representative set of stages could be something like:
0. No error decoration (e.g., using try
without any decoration).
1. Uniform error decoration (e.g., using try
+ defer
for uniform decoration).
2. N-1 exit points have uniform error decoration, but 1 exit point has different decoration (e.g., perhaps a permanent detailed error decoration in just one location, or perhaps a temporary debug log, etc.).
3. All exit points each have unique error decoration, or something approaching unique.
Any given function is not going to have a strict progression through those stages, so maybe “stages” is the wrong word, but some functions will transition from one decoration style to another, and it could be useful to be more explicit about what those transitions are like when or if they happen.
Stage 0 and stage 1 seem to be sweet spots for the current proposal, and also happen to be fairly common use cases. A stage 0->1 transition seems straightforward. If you were using try
without any decoration in stage 0, you can add something like defer fmt.HandleErrorf(&err, "foo failed with %s", arg1)
. You might at that moment also need to introduce named return parameters under the proposal as initially written. However, if the proposal adopts one of the suggestions along the lines of a predefined built-in variable that is an alias for the final error result parameter, then the cost and risk of error here might be small?
On the other hand, a stage 1->2 transition seems awkward (or “annoying” as some others have said) if stage 1 was uniform error decoration with a defer
. To add one specific bit of decoration at one exit point, first you would need to remove the defer
(to avoid double decoration), then it seems one would need to visit all the return points to desugar the try
uses into if
statements, with N-1 of the errors getting decorated the same way and 1 getting decorated differently.
A stage 1->3 transition also seems awkward if done manually.
Some mistakes that might happen as part of a manual desugaring process include accidentally shadowing a variable, or changing how a named return parameter is affected, etc. For example, if you look at the first and largest example in the “Examples” section of the try proposal, the CopyFile
function has 4 try
uses, including in this section:
w := try(os.Create(dst))
defer func() {
w.Close()
if err != nil {
os.Remove(dst) // only if a “try” fails
}
}()
If someone did an “obvious” manual desugaring of w := try(os.Create(dst))
, that one line could be expanded to:
w, err := os.Create(dst)
if err != nil {
// do something here
return err
}
That looks good at first glance, but depending on what block that change is in, that could also accidentally shadow the named return parameter err
and break the error handling in the subsequent defer
.
To help with the time cost and risk of mistakes, perhaps gopls
(or another utility) could have some type of command to desugar a specific try
, or a command desugar all uses of try
in a given func that could be mistake-free 100% of the time. One approach might be any gopls
commands only focus on removing and replacing try
, but perhaps a different command could desugar all uses of try
while also transforming at least common cases of things like defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
at the top of the function into the equivalent code at each of the former try
locations (which would help when transitioning from stage 1->2 or stage 1->3). That is not a fully baked idea, but perhaps worth more thought as to what is possible or desirable or updating the proposal with current thinking.
A related comment is it is not immediately obvious is how frequently a programmatic mistake free transformation of a try
would end up looking like normal idiomatic Go code. Adapting one of the examples from the proposal, if for example you wanted to desugar:
x1, x2, x3 = try(f())
In some cases, a programmatic transform that preserves behavior could end up with something like:
t1, t2, t3, te := f() // visible temporaries
if te != nil {
return x1, x2, x3, te
}
x1, x2, x3 = t1, t2, t3
That exact form might be rare, and it seems the results of an editor or IDE doing programatic desugaring could often end up looking more idiomatic, but it would be interesting to hear how true that is, including in the face of named return parameters possibly becoming more common, and taking into account shadowing, :=
vs =
, other uses of err
in the same function, etc.
The proposal talks about possible behavior differences between if
and try
due to named result parameters, but in that particular section it seems to be talking mainly about transitioning from if
to try
(in the section that concludes “While this is a subtle difference, we believe cases like these are rare. If current behavior is expected, keep the if statement.”). In contrast, there might be different possible mistakes worth elaborating when transitioning from try
back to if
while preserving identical behavior.
In any event, sorry for the long comment, but it seems a fear of high transition costs between styles is underlying some of the concern expressed in some of the other comments posted here, and hence the suggestion to be more explicit about those transition costs and potential mitigations.
@thepudds I love you are highlighting the costs and potential bugs associated with how language features can either positive or negatively affect refactoring. It is not a topic I see often discussed, but one that can have a large downstream effect.
a stage 1->2 transition seems awkward if stage 1 was uniform error decoration with a defer. To add one specific bit of decoration at one exit point, first you would need to remove the defer (to avoid double decoration), then it seems one would need to visit all the return points to desugar the try uses into if statements, with N-1 of the errors getting decorated the same way and 1 getting decorated differently.
This is where using break
instead of return
shines with 1.12. Use it in a for range once { ... }
block where once = "1"
to demarcate the sequence of code that you might want to exit from and then if you need to decoration just one error you do it at the point of break
. And if you need to decorate all errors you do it just before the sole return
at the end of the method.
The reason it is such a good pattern is it is resilient to changing requirements; you rarely ever have to break working code to implement new requirements. And it is a cleaner and more obvious approach IMO than jumping back to the beginning of the method before then jumping out of it.
#fwiw
…
At the time of this writing the 58%
down votes to 42%
up votes. I think this alone should be enough to indicate that this is divisive enough of a proposal that it is time to return to the drawing board on this issue.
Since this thread is getting long and hard to follow (and starts to repeat itself to a degree), I think we all would agree we would need to compromise on “some of the upsides any proposal offers.
As we keep liking or disliking the proposed code permutations above, we are not helping ourselves getting a real sense of “is this a more sensible compromise than another/whats already been offered” ?
I think we need some objective criteria rate our “try” variations and alt-proposals. - Does it decrease the boilerplate ? - Readability - Complexity added to the language - Error standardization - Go-ish … … - implementation effort and risks …
We can of course also set some ground rules for no-go’s (no backward compatibility would be one) , and leave a grey area for “does it look appealing/gut feeling etc (the “hard” criteria above can also be debatable…).
If we test any proposal against this list, and rate each point (boilerplate 5 point , readability 4 points etc), then instead I think we can align on: Our options are probably A,B and C, moreover, someone wishing to add a new proposal, could test (to a degree) if his proposal meets the criteria.
If this makes sense, thumb this up, we can try to go over the original proposal https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md
And perhaps some of the other proposals inline the comments or linked, perhaps we would learn something, or even come up with a mix that would rate higher.
Criteria += reuse of error handling code, across package & within function
Thanks everybody for the continued feedback on this proposal.
The discussion has veered off a bit from the core issue. It also has become dominated by a dozen or so contributors (you know who you are) hashing out what amounts to alternative proposals.
So let me just put out a friendly reminder that this issue is about a specific proposal. This is not a solicitation of novel syntactic ideas for error handling (which is a fine thing to do, but it’s not this issue).
Let’s get the discussion more focussed again and back on track.
Feedback is most productive if it helps identifying technical facts that we missed, such as “this proposal doesn’t work right in this case” or “it will have this implication that we didn’t realize”.
For instance, @magical pointed out that the proposal as written wasn’t as extensible as claimed (the original text would have made it impossible to add a future 2nd argument). Luckily this was a minor problem that was easily addressed with a small adjustment to the proposal. His input directly helped making the proposal better.
@crawshaw took the time to analyze a couple hundred use cases from the std library and showed that try
rarely ends up inside another expression, thus directly refuting the concern that try
might become buried and invisible. That is very useful fact-based feedback, in this case validating the design.
In contrast, personal aesthetic judgements are not very helpful. We can register that feedback, but we can’t act upon it (besides coming up with another proposal).
Regarding coming up with alternative proposals: The current proposal is the fruit of a lot of work, starting with last year’s draft design. We have iterated on that design multiple times and solicited feedback from many people before we felt comfortable enough to post it and recommending advancing it to the actual experiment phase, but we haven’t done the experiment yet. It does make sense to go back to the drawing board if the experiment fails, or if feedback tells us in advance that it will clearly fail. If we redesign on the fly, based on first impressions, we’re just wasting everybody’s time, and worse, learn nothing in the process.
All that said, the most significant concern voiced by many with this proposal is that it doesn’t explicitly encourage error decoration besides what we can do already in the language. Thank you, we have registered that feedback. We have received the very same feedback internally, before posting this proposal. But none of the alternatives we have considered are better than what we have now (and we have looked a many in depth). Instead we have decided to propose a minimal idea which addresses one part of error handling well, and which can be extended if need be, exactly to address this concern (the proposal talks about this at length).
Thanks.
(I note that a couple of people advocating for alternative proposals have started their own separate issues. That is a fine thing to do and helps keeping the respective issues focussed. Thanks.)
@griesemer I totally agree we should focus and that’s exactly what brought me to write:
If this makes sense, thumb this up, we can try to go over the original proposal https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md
Two questions: 1. Do you agree if we mark the upsides ( boilerplate reduction, readability, etc) vs downsides (no explicit error decoration/lower trace-ability of the error line source) we can actually state : this proposal highly aims to solve a,b , somewhat help c, does not aim to solve d,e And by that lose all clutter of “but it doesnt d” , “how can it e” and get it more towards technical issues such as @magical pointed out And also dis-encourage comments of “but solution XXX solves d,e better. 2. many inline posts are “suggestions for minor changes in the proposal” - I know its a fine line, but I think it does make sense to keep those.
LMKWYT.
@guybrand Upvoting and down-voting is a fine thing to express sentiment - but that is about it. There is no more information in there. We are not going to make a decision based on vote count, i.e. sentiment alone. Of course, if everybody - say 90%+ - hates a proposal, that is probably a bad sign and we should think twice before moving ahead. But that does not appear to be the case here. A good number of people seem to be happy with try-ing things out, and have moved on to other things (and don’t bother to comment on this thread).
As I tried to express above, sentiment at this stage of the proposal is not based on any actual experience with the feature; it’s a feeling. Feelings tend to change over time, especially when one had a chance to actually experience the subject the feelings are about… :-)
@griesemer I think you totally misunderstood me. I’m not looking into up voting/down voting this or any proposal, I was just looking into a way to get a good sense of “Do we think it makes sense to take a decision based on hard criteria rather than ‘I like x’ or ‘y doesnt look nice’ “
From what you wrote - thats EXACTLY what you think… so please upvote my comment as in saying:
“I think we should set up a list of what this proposal aims to improve, and based on that we can A. decide if that’s meaningful enough B. decide if it looks like proposal really solves what it aims to solve C. (as you added) make the extra effort trying to see if its feasible…
@guybrand they’re evidently convinced it’s worth prototyping in pre-release 1.14(?) and collecting feedback from hands-on users. IOW a decision has been made.
Also, filed #32611 for discussion of on err, <statement>
@guybrand My apologies. Yes, I agree we need to look at the various properties of a proposal, such as boilerplate reduction, does it solve the problem at hand, etc. But a proposal is more than the sum of its parts - at the end of the day we need to look at the overall picture. This is engineering, and engineering is messy: There are many factors that play into a design, and even if objectively (based on hard criteria) a part of a design is not satisfactory, it may still be the “right” design overall. So I am little hesitant to support a decision based on some sort of independent rating of the individual aspects of a proposal.
(Hopefully this addresses better what you meant.)
But regarding the relevant criteria, I believe this proposal makes it clear what it tries to address. That is, the list you are referring to already exists:
…, our goal is to make error handling more lightweight by reducing the amount of source code dedicated solely to error checking. We also want to make it more convenient to write error handling code, to raise the likelihood programmers will take the time to do it. At the same time we do want to keep error handling code explicitly visible in the program text.
It just so happens that for error decoration we suggest to use a defer
and named result parameters (or ye olde if
statement) because that doesn’t need a language change - which is a fantastic thing because language changes have enormous hidden costs. We do get that plenty of commenters feel that this part of the design “totally sucks”. Still, at this point, in the overall picture, with all we know, we think it may be good enough. On the other hand, we need a language change - language support, rather - to get rid of the boilerplate, and try
is about as minimal a change we could come up with. And clearly, everything is still explicit in the code.
The detailed proposal is now here (pending formatting improvements, to come shortly) and will hopefully answer a lot of questions.
One clarification / suggestion for improvement:
if the last argument supplied to try, of type error, is not nil, the enclosing function’s error result variable (...) is set to that non-nil error value before the enclosing function returns
Could this instead say is set to that non-nil error value and the enclosing function returns
? (s/before/and)
On first reading, before the enclosing function returns
seemed like it would eventually set the error value at some point in the future right before the function returned - possibly in a later line. The correct interpretation is that try may cause the current function to return. That’s a surprising behavior for the current language, so a clearer text would be welcomed.
@nictuku: Regarding your suggestion for clarification (s/before/and/): I think the code immediately before the paragraph you’re referring to makes it clear what happens exactly, but I see your point, s/before/and/ may make the prose clearer. I’ll make the change.
See CL 180637.
This is not a comment on the proposal, but a typo report. It wasn’t fixed since the full proposal was published, so I thought I’d mention it:
func try(t1 T1, t1 T2, … tn Tn, te error) (T1, T2, … Tn)
should be:
func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)
@politician, sorry, but the word you are looking for is not social but opinionated. Go is an opinionated programming language. For the rest I mostly agree with what you are getting at.
@beoran Community tools like Godep and the various linters demonstrate that Go is both opinionated and social, and many of the dramas with the language stem from that combination. Hopefully, we can both agree that try
shouldn’t be the next drama.
@politician Thanks for clarifying, I hadn’t understood it that way. I can certainly agree that we should try to avoid drama.
…
One last thing:
If you read the proposal you would see there is nothing preventing you from
Can we please refrain from such language? Of course I read the proposal. It just so happens that I read it last night and then commented this morning after thinking about it and didn’t explain the minutia of what I intended. This is an incredibly adversarial tone.
@cpuguy83 My bad cpu guy. I didn’t mean it that way.
And I guess you gotta point that code that uses try
will look pretty different from code that doesn’t, so I can imagine that would affect the experience of parsing that code, but I can’t totally agree that different means worse in this case, though I understand you personally don’t like it just as I personally do like it. Many things in Go are that way. As to what linters tell you to do is another matter entirely, I think.
Sure it’s not objectively better. I was expressing that it was more readable that way to me. I carefully worded that.
Again, sorry for sounding that way. Although this is an argument I didn’t mean to antagonize you.
This issue has gotten a lot of comments very quickly, and many of them seem to me to be repeating comments that have already been made. Of course feel free to comment, but I would like to gently suggest that if you want to restate a point that has already been made, that you do so by using GitHub’s emojis, rather than by repeating the point. Thanks.
The
check-handle
idea forced you to write a return statement, so writing a error wrapping was pretty trivial.
That isn’t true - in the design draft, every function that returns an error has a default handler which just returns the error.
That isn’t true - in the design draft, every function that returns an error has a default handler which just returns the error.
My bad, you are correct.
…
Instead of just saying "well, if you don't like it, don't use it and shut up" (this is how it reads), I think it would be better to try to address what many of use are considering a flaw in the design. Can we discuss instead what could be modified from the proposed design so that our concern handled in a better way?
…
Instead of just saying “well, if you don’t like it, don’t use it and shut up” (this is how it reads), I think it would be better to try to address what many of use are considering a flaw in the design.
If that is how it reads I apologize. My point isn’t that you should pretend that it doesn’t exist if you don’t like it. It’s that it’s obvious that there are cases in which try
would be useless and that you shouldn’t use it in such cases, which for this proposal I believe strikes a good balance between KISS and general utility. I didn’t think that I was unclear on that point.
@marwan-at-work, shouldn’t try(err, "someFunc failed")
be try(&err, "someFunc failed")
in your example?
@dpinela thank you for the correction, updated the code :)
Hi everyone. Thank you for the calm, respectful, constructive discussion so far. I spent some time taking notes and eventually got frustrated enough that I built a program to help me maintain a different view of this comment thread that should be more navigable and complete than what GitHub shows. (It also loads faster!) See https://swtch.com/try.html. I will keep it updated but in batches, not minute-by-minute. (This is a discussion that requires careful thought and is not helped by “internet time”.)
I have some thoughts to add, but that will probably have to wait until Monday. Thanks again.
@rsc, super useful! If you’re still revising it, maybe linkify the #id issue refs? And font-style sans-serif?
Re swtch.com/try.html and https://github.com/golang/go/issues/32437#issuecomment-502192315:
@rsc, super useful! If you’re still revising it, maybe linkify the #id issue refs? And font-style sans-serif?
That page is about content. Don’t focus on the rendering details. I’m using the output of blackfriday on the input markdown unaltered (so no GitHub-specific #id links), and I am happy with the serif font.
I like this proposal overall. The interaction with
defer
seems sufficient to provide an ergonomic way of returning an error while also adding additional context. Though it would be nice to address the snag @josharian pointed out around how to include the original error in the wrapped error message.