go: Proposal: A built-in Go error check function, "try"

Proposal: A built-in Go error check function, try

This proposal has been closed. Thanks, everybody, for your input.

Before commenting, please read the detailed design doc and see the discussion summary as of June 6, the summary as of June 10, and most importantly the advice on staying focussed. Your question or suggestion may have already been answered or made. Thanks.

We propose a new built-in function called try, designed specifically to eliminate the boilerplate if statements typically associated with error handling in Go. No other language changes are suggested. We advocate using the existing defer statement and standard library functions to help with augmenting or wrapping of errors. This minimal approach addresses most common scenarios while adding very little complexity to the language. The try built-in is easy to explain, straightforward to implement, orthogonal to other language constructs, and fully backward-compatible. It also leaves open a path to extending the mechanism, should we wish to do so in the future.

[The text below has been edited to reflect the design doc more accurately.]

The try built-in function takes a single expression as argument. The expression must evaluate to n+1 values (where n may be zero) where the last value must be of type error. It returns the first n values (if any) if the (final) error argument is nil, otherwise it returns from the enclosing function with that error. For instance, code such as

f, err := os.Open(filename)
if err != nil {
	return …, err  // zero values for other results, if any
}

can be simplified to

f := try(os.Open(filename))

try can only be used in a function which itself returns an error result, and that result must be the last result parameter of the enclosing function.

This proposal reduces the original draft design presented at last year’s GopherCon to its essence. 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:

defer func() {
	if err != nil {	// no error may have occurred - check for it
		err = …	// wrap/augment error
	}
}()

Here, err is the name of the error result of the enclosing function. In practice, suitable helper functions will reduce the declaration of an error handler to a one-liner. For instance

defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)

(where fmt.HandleErrorf decorates *err) reads well and can be implemented without the need for new language features.

The main drawback of this approach is that the error result parameter needs to be named, possibly leading to less pretty APIs. Ultimately this is a matter of style, and we believe we will adapt to expecting the new style, much as we adapted to not having semicolons.

In summary, try may seem unusual at first, but it is simply syntactic sugar tailor-made for one specific task, error handling with less boilerplate, and to handle that task well enough. As such it fits nicely into the philosophy of Go. try is not designed to address all error handling situations; it is designed to handle the most common case well, to keep the design simple and clear.

Credits

This proposal is strongly influenced by the feedback we have received so far. Specifically, it borrows ideas from:

Detailed design doc

https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

tryhard tool for exploring impact of try

https://github.com/griesemer/tryhard

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 1440
  • Comments: 812 (331 by maintainers)

Most upvoted comments

Hi everyone,

Our goal with proposals like this one is to have a community-wide discussion about implications, tradeoffs, and how to proceed, and then use that discussion to help decide on the path forward.

Based on the overwhelming community response and extensive discussion here, we are marking this proposal declined ahead of schedule.

As far as technical feedback, this discussion has helpfully identified some important considerations we missed, most notably the implications for adding debugging prints and analyzing code coverage.

More importantly, we have heard clearly the many people who argued that this proposal was not targeting a worthwhile problem. We still believe that error handling in Go is not perfect and can be meaningfully improved, but it is clear that we as a community need to talk more about what specific aspects of error handling are problems that we should address.

As far as discussing the problem to be solved, we tried to lay out our vision of the problem last August in the “Go 2 error handling problem overview,” but in retrospect we did not draw enough attention to that part and did not encourage enough discussion about whether the specific problem was the right one. The try proposal may be a fine solution to the problem outlined there, but for many of you it’s simply not a problem to solve. In the future we need to do a better job drawing attention to these early problem statements and making sure that there is widespread agreement about the problem that needs solving.

(It is also possible that the error handling problem statement was entirely upstaged by publishing a generics design draft on the same day.)

On the broader topic of what to improve about Go error handling, we would be very happy to see experience reports about what aspects of error handling in Go are most problematic for you in your own codebases and work environments and how much impact a good solution would have in your own development. If you do write such a report, please post a link on the Go2ErrorHandlingFeedback page.

Thank you to everyone who participated in this discussion, here and elsewhere. As Russ Cox has pointed out before, community-wide discussions like this one are open source at its best. We really appreciate everyone’s help examining this specific proposal and more generally in discussing the best ways to improve the state of error handling in Go.

Robert Griesemer, for the Proposal Review Committee.

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 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.

Thank you, Go Team, for the work that went into the try proposal. And thanks to the commenters who struggled with it and proposed alternatives. Sometimes these things take on a life of their own. Thank you Go Team for listening and responding appropriately.

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.

func M() (Data, error){ a, err1 := A() b, err2 := B() return b, nil } => (if err1 != nil){ return a, err1}. (if err2 != nil){ return b, err2}

  1. Echoing @deanveloper above, as well as others’ similar comments, I’m very afraid we’re underestimating the costs of adding a new, somewhat subtle, and—especially when inlined in other function calls—easily overlooked keyword that manages call stack control flow. 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.
  2. This proposal would canonize returning an un-wrapped error as the default behavior, and relegate wrapping errors as something you have to opt-in to, with additional ceremony. But, in my experience, that’s precisely backwards to good practice. I’d hope that a proposal in this space would make it easier, or at least not more difficult, to add contextual information to errors at the error site.

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.

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

image

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.

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
}

Instead of:

f := try(os.Open(filename))

I’d expect:

f := try os.Open(filename)

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.

  2. It gives us places where the code can exit from the middle of a statement.

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.

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).

The try() concept has all the problems of check from check/handle:

  1. 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
    
  2. 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++)

  3. It would be trivial to return errors without context. A key rationale of check/handle was to encourage contextualization.

  4. 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 { ... }

  5. 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:

    • log.Fatal()
    • panic() for errors that should never arise
    • log a message and retry

@gopherbot add Go2, LanguageChange

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).

  1. 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.

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.

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

First of all, thanks everybody for the supportive feedback on the final decision, even if that decision was not satisfactory for many. This was truly a team effort, and I’m really happy that we all managed to get through the intense discussions in an overall civil and respectful way.

@ngrilly Speaking just for myself, I still think it would be nice to address error handling verbosity at some point. That said, we have just dedicated quite a bit of time and energy on this over the last half year and especially the last 3 months, and we were quite happy with the proposal, yet we have obviously underestimated the possible reaction towards it. Now it does make a lot of sense to step back, digest and distill the feedback, and then decide on the best next steps.

Also, realistically, since we don’t have unlimited resources, I see thinking about language support for error handling go on the back-burner for a bit in favor of more progress on other fronts, most notably work on generics, at least for the next few months. if err != nil may be annoying, but it’s not a reason for urgent action.

If you like to continue the discussion, I would like to gently suggest to everybody to move off from here and continue the discussion elsewhere, in a separate issue (if there’s a clear proposal), or in other forums better suited for an open discussion. This issue is closed, after all. Thanks.

@ianlancetaylor

I think this proposal did provide a significant benefit in reduced verbosity, but clearly there is a significant segment of Go programmers who feel that the additional costs it imposed were too high.

I’m afraid there is a self-selection bias. Go is known for its verbose error handling, and its lack of generics. This naturally attracts developers that don’t care about these two issues. In the meantime, other developers keep using their current languages (Java, C++, C#, Python, Ruby, etc.) and/or switch to more modern languages (Rust, TypeScript, Kotlin, Swift, Elixir, etc.) because of this. I know many developers who avoid Go mostly for this reason.

I also think there is a confirmation bias at play. Gophers have been used to defend the verbose error handling and the lack of error handling when people criticize Go. This makes harder to objectively assess a proposal like try.

Steve Klabnik published an interesting comment on Reddit a few days ago. He was against introducing ? in Rust, because it was “two ways to write the same thing” and it was “too implicit”. But now, after having written more than a few lines of code with, ? is one of his favorite features.

@griesemer Just wanted to say thank you (and probably many others who worked with you) for your patience and efforts.

Luckily, go is not designed by committee. We need to trust that the custodians of the language we all love will continue to make the best decision given all the data available to them, and will not make a decision based on popular opinion of the masses. Remember - they use go also, just like us. They feel the pain points, just like us.

If you have a position, take the time to defend it like the way the Go team defends their proposals. Else you are just drowning the conversation with fly-by-night sentiments that are not actionable and do not carry the conversations forward. And it makes it harder for folks that want to engage, as said folks may just want to wait it out until the noise dies down.

When the proposal process started, Russ made a big deal about evangelizing the need for experience reports as a way to influence a proposal or make your request heard. Let’s at least try to honor that.

The go team has been taking all actionable feedback into consideration. They haven’t failed us yet. See the detailed documents produced for alias, for modules, etc. Let’s at least give them the same regard and spend time to think through our objections, respond to their position on your objections, and make it harder for your objection to be ignored.

Go’s benefit has always been that it is a small, simple language with orthogonal constructs designed by a small group of folks who would think through the space critically before committing to a position. Let’s help them where we can, instead of just saying “see, popular vote says no” - where many folks voting may not even have much experience in go or understand go fully. I’ve read serial posters who admitted that they do not know some foundational concepts of this admittedly small and simple language. That makes it hard to take your feedback seriously.

Anyway, sucks that I’m doing this here - feel free to remove this comment. I will not be offended. But someone has to say this bluntly!

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.

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.

So, because of all of this, I’m strongly against this proposal.

我只是一个普通的 go 开发者,并且我也没有做很多底层的开发,主要用 go 来做web开发。从使用感觉上来讲,go 的错误处理机制真的很难用。真的很难理解为什么不用try catch?实现一套异常错误处理机制,就像大部分的语言一样。

这样用不是更好吗?(我自己使用 recover 机制实现了一个try catch)

  1. 所有的函数将不会有错误返回值
  2. 可以根据不同的异常做不同的处理
  3. 代码大量减少

	response := NewJsonResponse(w, r)
	Try(func() {
         ...
           方法可能抛出各种异常
	 ...
	}).Catch(exception.NotLoggedIn{}, func(err error) {
		response.NeedLogin()
	}).Catch(exception.LoginExpired{}, func(err error) {
		response.LoginExpire()
	}).Catch(exception.NoPermission{}, func(err error) {
		response.PermissionDenied()
	}).Catch(exception.RedisBroken{}, func(err error) {
		response.RedisBroken()
	}).Catch(&httpErrs.HTTPError{}, func(err error) {
		lego.Error("系统可能正在受到 CC 攻击, 请求来源 ", r.RemoteAddr)
		response.RequestSpeedTooFast()
	}).CatchAll(func(err error) {
		response.SysError()
	}).Finally(func() {})

英文很差,见谅~

一点猜想

try catch 大概率不会被官方支持了,golang 的错误处理机制,积重难返。如果使用 try catch 这样的机制,golang的官方包,三方包统统都要改造~

It’s an absolutely bad idea.

  • When does err var appear for defer? What about “explicit better than implicit”?
  • We use a simple rule: you should quickly find an exactly one place where you have error returned. Every error is wrapped with context to understand what and where goes wrong. defer will create a lot of ugly and hard-to-understand code.
  • @davecheney wrote a great post about errors and the proposal is fully against everything in this post.
  • At last, if you use os.Exit, your errors will be unchecked.

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.

  1. On the positive side, @rasky, @adg, @eandre, @dpinela, and others explicitly expressed happiness over the code simplification that try provides.

  2. 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.)

  3. 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”.

  4. 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.

  5. 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).

  6. There’s some concern about the use of named returns (@buchanae, @adg).

Several people have provided suggestions to improve or modify the proposal:

  1. 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.

  2. @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).

  3. @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).

  4. 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):

  1. Thanks for the positive feedback! 😃

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. Optional handler argument to try: The detailed document discusses this as well. See the section on Design iterations.

  8. 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.

  9. 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.

  10. 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.

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”.

bad. this is anti pattern, disrespect author of that proposal

Thanks to the team and community for engaging on this. I love how many people care about Go.

I really hope the community sees first the effort and skill that went into the try proposal in the first place, and then the spirit of the engagement that followed that helped us reach this decision. The future of Go is very bright if we can keep this up, especially if we can all maintain positive attitudes.

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.

  1. 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.

  2. 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.

  3. 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.

  1. 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.

  2. 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).

  3. 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.

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.)

@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.

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

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:

  • When evaluating this proposal, it would allow people to quickly get a sense for how try could be used in their codebase.
  • If 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.
  • Having a way to quickly transform a large codebase to use 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.

I don’t want to be rude here, and I appreciate all your moderation, but the community has spoken extremely strongly about error handling being changed. Changing things, or adding new code, will upset all the people who prefer the current system. You can’t make everyone happy, so let’s focus on the 88% we can make happy (number derived from the vote ratio below).

At the time of this writing, the “leave it alone” thread is at 1322 votes up and 158 down. This thread is at 158 up and 255 down. If that isn’t a direct end of this thread on error handling, then we should have a very good reason to keep pushing the issue.

It is possible to always do what your community screams for and to destroy your product at the same exact time.

At a minimum, I think this specific proposal should be considered as failed.

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.

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”:
    a, b := try SomeFunc() else err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }
    
    becomes
    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.

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.

@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)
        }

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.

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.

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.

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.

@gbbr You have a choice here. You could write it as:

slurp := try(ioutil.ReadFile(path))
return ioutil.WriteFile(path, append(copyrightText, slurp...), 0666)

which still saves you a lot of boilerplate yet makes it much clearer. This is not inherent to try. Just because you can squeeze everything into a single expression doesn’t mean you should. That applies generally.

Here are the statistics of running tryhard over our internal codebase (only our code, not dependencies):

Before:

  • 882 .go files
  • 352434 loc
  • 329909 non-empty loc

After tryhard:

  • 2701 replacements (avg 3.1 replacements / file)
  • 345364 loc (-2.0%)
  • 322838 non-empty loc (-2.1%)

Edit: Now that @griesemer updated tryhard to include summary statistics, here are a couple more:

  • 39.2% of if statements are if <err> != nil
  • 69.6% of these are try candidates

Looking through the replacements that tryhard found, there are certainly types of code where the usage of try would be very prevalent, and other types where it would be rarely used.

I also noticed some places that tryhard could not transform, but would benefit greatly from try. For instance, here is some code we have for decoding messages according to a simple wire protocol (edited for simplicity/clarity):

func (req *Request) Decode(r Reader) error {
	typ, err := readByte(r)
	if err != nil {
		return err
	}
	req.Type = typ
	req.Body, err = readString(r)
	if err != nil {
		return unexpected(err)
	}

	req.ID, err = readID(r)
	if err != nil {
		return unexpected(err)
	}
	n, err := binary.ReadUvarint(r)
	if err != nil {
		return unexpected(err)
	}
	req.SubIDs = make([]ID, n)
	for i := range req.SubIDs {
		req.SubIDs[i], err = readID(r)
		if err != nil {
			return unexpected(err)
		}
	}
	return nil
}

// unexpected turns any io.EOF into an io.ErrUnexpectedEOF.
func unexpected(err error) error {
	if err == io.EOF {
		return io.ErrUnexpectedEOF
	}
	return err
}

Without try, we just wrote unexpected at the return points where it’s needed since there’s no great improvement by handling it in one place. However, with try, we can apply the unexpected error transformation with a defer and then dramatically shorten the code, making it clearer and easier to skim:

func (req *Request) Decode(r Reader) (err error) {
	defer func() { err = unexpected(err) }()

	req.Type = try(readByte(r))
	req.Body = try(readString(r))
	req.ID = try(readID(r))

	n := try(binary.ReadUvarint(r))
	req.SubIDs = make([]ID, n)
	for i := range req.SubIDs {
		req.SubIDs[i] = try(readID(r))
	}
	return nil
}

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)

(might make sense to lock this thread as well…)

This also closes the gates for having exceptions using try/catch forever.

I am more than okay with this.

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)
  }
}

I feel like all the important feedback against the try() proposal was voiced already. But let me try to summarize:

  1. try() moves vertical code complexity to horizontal
  2. Nested try() calls are as hard to read as ternary operators
  3. Introduces invisible ‘return’ control flow that’s not visually distinctive (compared to indented blocks starting with return keyword)
  4. Makes error wrapping practice worse (context of function instead of a specific action)
  5. Splits #golang community & code style (anti-gofmt)
  6. Will make devs rewrite try() to if-err-nil and vice versa often (tryhard vs. adding cleanup logic / additional logs / better error context)

@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.

This proposal takes us from having 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.

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.

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:

https://github.com/purpleidea/mgmt/blob/a235b760dc3047a0d66bb0b9d63c25bc746ed274/util/errwrap/errwrap.go#L26

I really think this absolutely should not be a built-in function.

It has been mentioned a few time that panic() and recover() also alter the control flow. Very well, let us not add more.

@networkimprov wrote https://github.com/golang/go/issues/32437#issuecomment-498960081:

It doesn’t read like Go.

I couldn’t agree more.

If anything, I believe any mechanism for addressing the root problem (and I’m not sure there is one), it should be triggered by a keyword (or key symbol ?).

How would you feel if go func() were to be go(func()) ?

Since the debate here seems to continue unabated, let me repeat again:

Experience is now more valuable than continued discussion. We want to encourage people to take time to experiment with what try would look like in their own code bases and write and link experience reports on the feedback page.

If concrete experience provides significant evidence for or against this proposal, we’d like to hear that here. Personal pet peeves, hypothetical scenarios, alternative designs, etc. we can acknowledge but they are less actionable.

Thanks.

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”.

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.

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?

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.

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.

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.

I’m against try, on balance, with compliments to the contributors on the nicely minimal design. I’m not a heavy Go expert, but was an early adopter and have code in production here and there. I work in the Serverless group in AWS and it looks like we’ll be releasing a Go-based service later this year the first check-in of which was substantially written by me. I’m a really old guy, my path to go led through C, Perl, Java, and Ruby. My issues have appeared before in the very useful debate summary but I still think they’re worth reiterating.

  1. Go is a small, simple language and thus has achieved unequaled readability. I’m reflexively against adding anything to it unless the benefit is really qualitatively substantial. One usually doesn’t notice a slippery slope until one’s on it, so let’s not take the first step.
  2. I was quite strongly affected by the argument above about facilitating debugging. I like the visual rhythm, in low-level infrastructure code, of little stanzas of code along the lines of “Do A. Check if it worked. Do B. Check if it worked… etc" Because the “Check” lines are where you put the printf or the breakpoint. Maybe everyone else is smarter, but I end up using that breakpoint idiom regularly.
  3. Assuming named return values, “try” is about equivalent to if err != nil { return } (I think?) I personally like named return values and, given the benefits of error decorators, I suspect the proportion of named err return values is going to monotonically increase; which weakens the benefits of try.
  4. I initially liked the proposal to have gofmt bless the one-liner in the line above, but on balance, the IDEs will doubtless adopt this display idiom anyhow, and the one-liner would sacrifice the debug-here benefit.
  5. It seems quite likely that some forms of expression nesting containing “try” will open the door for the complicators in our profession to wreak the same kind of havoc they have with Java streams and spliterators and so on. Go has been more successful than most other languages at denying the clever among us opportunities to demonstrate their skillz.

Again, congratulations to the community on the nice clean proposal and constructive discussion.

@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)
        }

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 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.

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.

I’ve spent a significant amount of time jumping into and reading unfamiliar libraries or pieces of code over the last few years. Despite the tedium, if err != nil provides a very easy to read, albeit vertically verbose, idiom. The spirit of what try() is trying to accomplish is noble, and I do think there is something to be done, but this feature feels misprioritized and that the proposal is seeing the light of day too early (i.e. it should come after xerr and generics have had a chance to marinate in a stable release for 6-12mo).

Introducing try() appears to be a noble and worthwhile proposal (e.g. 29% - ~40% of if statements are for if err != nil checking). On the surface, it appears as though the reducing boilerplate associated with error handling will improve developer experiences. The tradeoff from the introduction of try() comes in the form of cognitive load from the semi-subtle special-cases. One of Go’s biggest virtues is that it’s simple and there is very little cognitive load required to get something done (compared to C++ where the language spec is large and nuanced). Reducing one quantitative metric (LoC of if err != nil) in exchange for increasing the quantitative metric of mental complexity is a tough pill to swallow (i.e. the mental tax on the most precious resource we have, brain-power).

In particular the new special cases for the way try() is handled with go, defer, and named return variables makes try() magical enough to make the code less explicit such that all authors or readers of Go code will have to know these new special cases in order to properly read or write Go and such burden did not exist previously. I like that there are explicit special cases for these situations - especially versus introducing some form of undefined behavior, but the fact that they need to exist in the first place indicates this is incomplete at the moment. If the special cases were for anything but error handling, it could be possible acceptable, but if we’re already talking about something that could impact up to 40% of all LoC, these special cases will need to be trained into the entire community and that raises the cost of the cognitive load of this proposal to a high-enough level to warrant concern.

There is another example in Go where special-case rules are already a slippery cognitive slope, namely pinned and unpinned variables. Needing to pin variables isn’t hard to understand in practice, but it gets missed because there is an implicit behavior here and this causes a mismatch between the author, reader, and what happens with the compiled executable at runtime. Even with linters such as scopelint many developers still don’t seem to grasp this gotcha (or worse, they know it but miss it because this gotcha slips their mind). Some of the most unexpected and difficult to diagnose runtime bugs from functioning programs have come from this particular problem (e.g. N objects all get populated with the same value instead of iterating over a slice and getting the expected distinct values). The failure domain from try() is different than pinned variables, but there will be an impact on how people write code as a result.

IMNSHO, the xerr and generics proposals need time to bake in production for 6-12mo before attempting to conquer the boilerplate from if err != nil. Generics will likely pave the way for more rich error handling and a new idiomatic way of error handling. Once idiomatic error handling with generics begins to emerge, then and only then, does it make sense to revisit a discussion around try() or whatever.

I don’t pretend to know how generics will impact error handling, but it seems certain to me that generics will be used to create rich types that will almost certainly be used in error handling. Once generics have permeated libraries and have been added to error handling there may be an obvious way to repurpose the try() to improve developer experience with regards to error handling.

The points of concern that I have are:

  1. try() isn’t complicated in isolation, but it’s cognitive overhead where none used to exist beforehand.
  2. By baking in err != nil into the assumed behavior of try(), the language is preventing the use of err as a way of communicating state up the stack.
  3. Aesthetically try() feels like forced cleverness but not clever enough to satisfy the explicit and obvious test that most of the Go language enjoys. Like most things involving subjective criteria, this is a matter of personal taste and experience and hard to quantify.
  4. Error handling with switch/case statements and error wrapping seems untouched by this proposal, and a missed opportunity, which leads me to believe this proposal is a brick shy of making an unknown-unknown a known-known (or at worst, a known-unknown).

Lastly, the try() proposal feels like a new break in the dam that was holding back a flood of language-specific nuance like what we escaped by leaving C++ behind.

TL;DR: isn’t a #nevertry response so much as it is, “not now, not yet, and let’s consider this again in the future after xerr and generics mature in the ecosystem.”

Let’s move on from this - we’ve heard it many times before

“This is so boring, let’s move on from this”

There is another good analogue:

- Your theory contradicts the facts! - The worse for the facts!

By Hegel

I mean you are solving a problem that doesn’t exists in fact. And the ugly way at that.

Let’s take a look at where this problem actually appears: handling side effects from the outer world, that’s it. And this actually is one of the easiest part logically in software engineering. And the most important at that. I cannot understand why do we need a simplification for the easiest thing which will cost us lesser reliability.

IMO the hardest problem of such kind is data consistency preservation in distributed systems (and not so distributed in fact). And error handling was not a problem I was fighting with in Go when solving these. Lack of slice and map comprehensions, lack of sum/algebraic/variance/whatever types was FAR more annoying.

Re disabling/vetting try:

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.

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.

if it != "broke" {
  dontFix(it)
}

Expression based flow control

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 defers 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.


Another proposal

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 }

Rationale

  • It requires no language changes
  • The formatting rule is simple and clear
  • The rule can be designed to be opt in where gofmt keeps newlines if they already exist (like struct literals). Opt in also allows the writer to make some error handling be emphasized
  • If it’s not opt in, code can be automatically ported to the new style with a call to gofmt
  • It’s only for return statements, so it won’t be abused to golf code unnecessarily
  • Interacts well with comments describing why some errors may happen and why they’re being returned. Using many nested try expressions handles this poorly
  • It reduces the vertical space of error handling by 66%
  • No expression based control flow
  • Code is read far more often than it’s written, so it should be optimized for the reader. Repetitive code taking up less space is helpful to the reader, where try leans more towards the writer
  • People have already been proposing try existing on multiple lines. For example this comment or this comment which introduces a style like
f, err := os.Open(file)
try(maybeWrap(err))
  • The “try on its own line” style removes any ambiguity about what 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 are
  • It doesn’t promote the use of named returns or unclear defer based wrapping. Both raise the barrier to wrapping errors and the former may require godoc changes
  • There doesn’t need to be discussion about when to use try versus using traditional error handling
  • Doesn’t preclude doing try or something else in the future. The change may be positive even if try is accepted
  • No negative interaction with the testing 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. Consider
value, err := something()
if err != nil { t.Fatal(err) }
  • No negative interaction with checking against specific errors. Consider
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

From https://github.com/golang/go/issues/32437#issuecomment-498941435

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
}

With this

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.

From https://github.com/golang/go/issues/32437#issuecomment-499007288

With try

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))
	// ...
}

With this

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.

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.

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.

So, late to the party, since this has already been declined, but for future discussion on the topic, what about a ternary-like conditional return syntax? (I didn’t see anything similar to this in my scan of the topic or looking over the view of it Russ Cox posted on Twitter.) Example:

f, err := Foo()
return err != nil ? nil, err

Returns nil, err if err is non-nil, continues execution if err is nil. The statement form would be

return <boolean expression> ? <return values>

and this would be syntactical sugar for:

if <boolean expression> {
    return <return values>
}

The primary benefits is that this is more flexible than a check keyword or try built-in function, because it can trigger on more than errors (ex. return err != nil || f == nil ? nil, fmt.Errorf("failed to get Foo"), on more than just the error being non-nil (ex. return err != nil && err != io.EOF ? nil, err), etc, while still being fairly intuitive to understand when read (especially for those used to reading ternary operators in other languages).

It also ensures that the error handling still takes place at call location, rather than automagically happening based on some defer statement. One of the biggest gripes I had with the original proposal is that it attempts to, in some ways, make the actual handling of errors an implicit processes that just happens automagically when the error is non-nil, with no clear indication that the control flow will return if the function call returns a non-nil error. The entire point of Go using explicit error returns instead of an exception-like system is to encourage developers to explicitly and intentionally check and handle their errors, rather than just letting them propagate up the stack to be, in theory, handled at some point higher up. At least an explicit, if conditional, return statement clearly annotates what’s going on.

This proves once again that the Go community is being heard and able to discuss controversial language change proposals. Like the changes that make it into the language, the changes that don’t are an improvement. Thank you, Go team and community, for the hard work and civilized discussion around this proposal!

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.

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.)
  • Existence of 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:

  • We might want to require that any label used as the target of a 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.
  • This would require fixing https://github.com/golang/go/issues/26058.

Credit: I believe a variant of this idea was first suggested by @griesemer in person at GopherCon last year.

How about use only ? to unwrap result just like rust

@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.

@sylr

How would you feel if go func() were to be go(func()) ?

Come on, that would be pretty acceptable.

@iand The answer given by @rsc is still valid. I’m not sure which part of that answer is “lacking substance” or what it takes to be “inspiring”. But let me try to add more “substance”:

The purpose of the proposal evaluation process is to ultimately identify “whether a change has delivered the expected benefits or created any unexpected costs” (step 5 in the process).

We have passed step 1: The Go team has selected specific proposals that seem worth accepting; this proposal is one of them. We would not have selected it if we had not thought about it pretty hard and deemed it worthwhile. Specifically, we do believe that there is a significant amount of boilerplate in Go code related solely to error handling. The proposal is also not coming out of thin air - we’ve been discussing this for over a year in various forms.

We are currently at step 2, thus still quite a bit away from a final decision. Step 2 is for gathering feedback and concerns - which there seem to be plenty of. But to be clear here: So far there was only a single comment pointing out a technical deficiency with the design, which we corrected. There were also quite a few comments with concrete data based on real code which indicated that try would indeed reduce boilerplate and simplify code; and there were a few comments - also based on data on real code - that showed that try would not help much. Such concrete feedback, based on actual data, or pointing out technical deficiencies, is actionable and very helpful. We will absolutely take this into account.

And then there was the vast amount of comments that is essentially personal sentiment. This is less actionable. This is not to say that we are ignoring it. But just because we are sticking to the process does not mean we are “tone-deaf”.

Regarding these comments: There are perhaps two, maybe three dozen vocal opponents of this proposal - you know who you are. They are dominating this discussion with frequent posts, sometimes multiple a day. There is little new information to be gained from this. The increased number of posts also does not reflect a “stronger” sentiment by the community; it just means that these people are more vocal than others.

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:

  1. People should follow the stated Go convention “callee adds relevant context it knows.”
  2. 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).
  3. Much Go code today does not add the function-level context because it is too repetitive.
  4. Providing a way to write the function-level context once will make it more likely that developers do that.
  5. The end result will be more Go code following the convention and adding appropriate context.

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.

@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.

@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).

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.

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.

val := try f() (err){ panic(err) }

Honestly this should be reopened, out of all the err handling proposals, this is the most sane one.

@ngrilly As @griesemer said, I think we need to better understand what aspects of error handling Go programmers find most problematic.

Speaking personally, I don’t think a proposal that removes a small amount of verbosity is worth doing. After all, the language works well enough today. Every change carries a cost. If we are going to make a change, we need a significant benefit. I think this proposal did provide a significant benefit in reduced verbosity, but clearly there is a significant segment of Go programmers who feel that the additional costs it imposed were too high. I don’t know whether there is a middle ground here. And I don’t know whether the problem is worth addressing at all.

Compact single-line if

As an addition to the single-line if proposal by @zeebo and others, the if statement could have a compact form that removes the != nil and the curly braces:

if err return err
if err return errors.Wrap(err, "foo: failed to boo")
if err return fmt.Errorf("foo: failed to boo: %v", err)

I think this is simple, lightweight & readable. There are two parts:

  1. Have if statements implicitly check error values for nil (or perhaps interfaces more generally). IMHO this improves readability by reducing density and the behaviour is quite obvious.
  2. Add support for if variable return .... Since the return is so close to the left-hand-side it seems to be still quite easy to skim the code - the extra difficulty of doing so being one of the main arguments against single-line ifs (?) Go also already has precedent for simplifying syntax by for example removing parentheses from its if statement.

Current style:

a, err := BusinessLogic(state)
if err != nil {
   return nil, err
}

One-line if:

a, err := BusinessLogic(state)
if err != nil { return nil, err }

One-line compact if:

a, err := BusinessLogic(state)
if err return nil, err
a, err := BusinessLogic(state)
if err return nil, errors.Wrap(err, "some context")
func (c *Config) Build() error {
	pkgPath, err := c.load()
	if err return nil, errors.WithMessage(err, "load config dir")

	b := bytes.NewBuffer(nil)
	err = templates.ExecuteTemplate(b, "main", c)
	if err return nil, errors.WithMessage(err, "execute main template")

	buf, err := format.Source(b.Bytes())
	if err return nil, errors.WithMessage(err, "format main template")

	target := fmt.Sprintf("%s.go", filename(pkgPath))
	err = ioutil.WriteFile(target, buf, 0644)
	if err return nil, errors.WithMessagef(err, "write file %s", target)

	// ...
}

I’ve uploaded a slightly improved version of tryhard. It now reports more detailed information on the input files. For instance, running against tip of the Go repo it reports now:

$ tryhard $HOME/go/src
...
--- stats ---
  55620 (100.0% of   55620) function declarations
  14936 ( 26.9% of   55620) functions returning an error
 116539 (100.0% of  116539) statements
  27327 ( 23.4% of  116539) if statements
   7636 ( 27.9% of   27327) if <err> != nil statements
    119 (  1.6% of    7636) <err> name is different from "err" (use -l flag to list file positions)
   6037 ( 79.1% of    7636) return ..., <err> blocks in if <err> != nil statements
   1599 ( 20.9% of    7636) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
     17 (  0.2% of    7636) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
   5907 ( 77.4% of    7636) try candidates (use -l flag to list file positions)

There’s more to be done, but this gives a clearer picture. Specifically, 28% of all if statements appear to be for error checking; this confirms that there is a significant amount of repetitive code. Of those error checks, 77% would be amenable to try.

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.

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

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
}

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.

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 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.

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.

Adding a few comments to my downvote.

For the specific proposal at hand:

  1. I would greatly prefer this to be a keyword vs. a built-in function for previously articulated reasons of control flow and code readability.

  2. Semantically, “try” is a lightning rod. And, unless there is an exception thrown, “try” would be better renamed to something like guard or ensure.

  3. Besides these two points, I think this is the best proposal I’ve seen for this sort of thing.

A couple more comments articulating my objection to any addition of a try/guard/ensure concept vs. leaving if err != nil alone:

  1. This runs counter to one of golang’s original mandates (at least as I perceived it) to be explicit, easy to read/understand, with very little ‘magic’.

  2. This will encourage laziness at the precise moment when thought is required: “what is the best thing for my code to do in the case of this error?”. There are many errors that can arise while doing “boilerplate” things like opening files, transferring data over a network, etc. While you may start out with a bunch of “trys” that ignore non-common failure scenarios, eventually many of these “trys” will go away as you may need to implement your own backoff/retry, logging/tracing, and/or cleanup tasks. “Low-probability events” are guaranteed at scale.

We have around a dozen tools written in a go at our company. I ran tryhard tool against our codebase and found 933 potential try() candidates. Personally, I believe try() function is a brilliant idea because it solves more than just code boilerplate issue.

It enforces both the caller and called function/method to return the error as the last parameter. This will not be allowed:

var file= try(parse())

func parse()(err, result) {
}

It enforces one way to deal with errors instead declaring the error variable and loosely allowing err!=nil err==nil pattern, which hinders readability, increases risk of error-prone code in IMO:

func Foo() (err error) {
	var file, ferr = os.Open("file1.txt")
	if ferr == nil {
               defer file.Close()
		var parsed, perr = parseFile(file)
		if perr != nil {
			return
		}
		fmt.Printf("%s", parsed)
	}
	return nil
}

With try(), code is more readable, consistent and safer in my opinion:


func Foo() (err error) {
	var file = try(os.Open("file.txt"))
        defer file.Close()
	var parsed = try(parseFile(file))
	fmt.Printf(parsed)
	return
}

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 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.

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 with return. 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:

  1. Panic is not conditional; it always aborts the calling function.
  2. Panic does not return any values and thus can only appear as a standalone statement, which increases it’s visibility.

Try on the other hand:

  1. Is conditional; it may or may not return from the calling function.
  2. Returns values and can appear in a compound expression, possibly multiple times, on a single line, potentially past the right margin of my editor window.

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.

@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

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()
}

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.

@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.

@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.

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.

Oh no, please don’t add this ‘magic’ into the langage. These doesn’t looks and feel like the rest of the langage. I already see code like this appearing everywhere.

a, b := try( f() )
if a != 0 && b != "" {
...
}
...

in stead of

a,b,err := f()
if err != nil {
...
}
...

or

a,b,_:= f()

The call if err.... patern was a little unnatural at the begining for me but now i’m used to I feel easier to deal with errors as they may arrive in the execution flow in stead of writing wrappers/handlers that will have to keep track of some kind of state to act once fired. And if i decide to ignore errors to save my keyboard’s life, i’m aware i’ll panic one day.

i even changed my habits in vbscript to :

on error resume next
a = f()
if er.number <> 0 then
    ...
end if
...

@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.

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.

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.

vs :

for _,fp := range filepaths {
    if f,err := os.Open(path);err!=nil{
        fmt.Printf( "Cannot open file %s\n", fp )
    }
}

?

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:

Try as an expression

// 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")))),
))

Try as a statement

// 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)

Takeaways

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.

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.

@ChrisHines To your point (which is echoed elsewhere in this thread), let’s add another restriction:

  • any 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 {}

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:

  • if they are not named zero value
  • the value assigned to the named variables otherwise

After spending hours reading all the comments and the detailed design doc I wanted to add my views to this proposal.

I will do my best to respect @ianlancetaylor’s request not to just restate previous points but to instead add new comments to the discussion. However I don’t think I can make the new comments without somewhat referencing prior comments.

Concerns

Unfortunate overloading of defer

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.

No stats verifying a significant use-case

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?

Because without known stats that this is appropriate to a large chuck of code in the wild I have to ask is this new complicating change to the language that will require everyone to learn the new concepts really address a compelling number of use-cases?

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?

Can (mostly) already implement try() in userland

Unless 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.)

Lack of obviousness

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 defers? 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?”

Lack of control

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; funcs 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.

Lack of stated support for 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.

I have used this approach for a while and its benefits in easing refactoring alone make it preferable to early return, but it has several other benefits including single exit point and ability to terminate a section of a func early but still be able to run cleanup (which is probably why I so rarely use defer, which I find harder to reason about in terms of program flow.)

To use break instead of early return use a for range "1" {...} loop to create a block for the break to exit from (I actually create a package called only that only contains a constant called Once with a value of "1"):

func (me *Config) WriteFile() (err error) {
	for range only.Once {
		var j []byte
		j, err = json.MarshalIndent(me, "", "    ")
		if err != nil {
			err = fmt.Errorf("unable to marshal config; %s", 
				err.Error(),
			)
			break
		}
		err = me.MaybeMakeDir(me.GetDir(), os.ModePerm)
		if err != nil {
			err = fmt.Errorf("unable to make directory'%s'; %s", 
				me.GetDir(), 
				err.Error(),
			)
			break
		}
		err = ioutil.WriteFile(string(me.GetFilepath()), j, os.ModePerm)
		if err != nil {
			err = fmt.Errorf("unable to write to config file '%s'; %s", 
				me.GetFilepath(), 
				err.Error(),
			)
			break
		}
	}
	return err
}

I plan to blog about the pattern at length in the near future, and discuss the several reasons why I have found it to work better than early returns.

But I digress. My reason from bringing it up here is I would have for Go to implement error handling that assumes early returns and ignores using break for error handling

My opinion err == nil is problematic

As 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.

“Not for everyone” justification

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:

  1. Indignation if it is a feature I do agree with, or
  2. Elation if it is a feature I do not agree with.

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?

Nesting of try()

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.

In summary

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.

#fwiw

P.S. To put it more tongue-in-cheek, I think we should follow the paraphrased wisdom of Yoda:

“There is no try(). Only do().”

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.

@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.


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.

This looks like a special cased macro.

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.

@piotrkowalczuk Your code looks much better than mine. I think the code can be more concise.

i, err := strconv.Atoi("1")
switch err {
case io.EOF:
    println("EOF")
case io.ErrShortWrite:
    println("ErrShortWrite")
} else {
    println(i)
}

I don’t agree on the decision. However I absolutely endorse approach the go team has undertaken. Having a community wide discussion and considering feedback from developers is what open source meant to be.

Here are some more raw tryhard stats. This is only lightly validated, so feel free to point out errors. 😉

First 20 “Popular Packages” on godoc.org

These are the repositories that correspond to the first 20 Popular Packages on https://godoc.org, sorted by try candidate percentage. This is using the default tryhard settings, which in theory should be excluding vendor directories.

The median value for try candidates across these 20 repos is 58%.

project loc if stmts if != nil (% of if) try candidates (% of if != nil)
github.com/google/uuid 1714 12 16.7% 0.0%
github.com/pkg/errors 1886 10 0.0% 0.0%
github.com/aws/aws-sdk-go 1911309 32015 9.4% 8.9%
github.com/jinzhu/gorm 15246 44 11.4% 20.0%
github.com/robfig/cron 1911 20 35.0% 28.6%
github.com/gorilla/websocket 6959 212 32.5% 39.1%
github.com/dgrijalva/jwt-go 3270 118 29.7% 40.0%
github.com/gomodule/redigo 7119 187 34.8% 41.5%
github.com/unixpickle/kahoot-hack 1743 52 75.0% 43.6%
github.com/lib/pq 13396 239 30.1% 55.6%
github.com/sirupsen/logrus 5063 29 17.2% 60.0%
github.com/prometheus/client_golang 17791 194 49.0% 62.1%
github.com/go-redis/redis 21182 326 42.6% 73.4%
github.com/mongodb/mongo-go-driver 86605 2097 37.8% 73.9%
github.com/uber-go/zap 15363 84 36.9% 74.2%
github.com/golang/protobuf 42959 685 22.9% 77.1%
github.com/gin-gonic/gin 14574 96 53.1% 86.3%
github.com/go-pg/pg 26369 831 37.7% 86.9%
github.com/Shopify/sarama 36427 1369 68.2% 91.0%
github.com/stretchr/testify 13496 32 43.8% 92.9%

The “if stmts” column is only tallying if statements in functions returning an error, which is how tryhard reports it, and which hopefully explains why it is so low for something like gorm.

10 misc. “large” Go projects

Given popular packages on godoc.org tend to be library packages, I wanted to also check stats for some larger projects as well.

These are misc. large projects that happened to be top-of-mind for me (i.e., no real logic behind these 10). This is again sorted by try candidate percentage.

The median value for try candidates across these 10 repos is 59%.

project loc if stmts if != nil (% of if) try candidates (% of if != nil)
github.com/juju/juju 1026473 26904 51.9% 17.5%
github.com/go-kit/kit 38949 467 57.0% 51.9%
github.com/boltdb/bolt 12426 228 46.1% 53.3%
github.com/hashicorp/consul 249369 5477 47.6% 54.5%
github.com/docker/docker 251152 8690 48.7% 56.8%
github.com/istio/istio 429636 7564 40.4% 61.9%
github.com/gohugoio/hugo 94875 1853 42.4% 64.8%
github.com/etcd-io/etcd 209603 4657 38.3% 65.5%
github.com/kubernetes/kubernetes 1789172 40289 43.3% 66.5%
github.com/cockroachdb/cockroach 1038529 22018 39.9% 74.0%

These two tables of course only represent a sample of open source projects, and only reasonably well known ones. I’ve seen people theorize that private code bases would show greater diversity, and there is at least some evidence of that based on some of the numbers that various people have been posting.

@nvictor

try is a clean and understandable solution to the specific problem it is trying to solve: verbosity in error handling.

Verbosity in error handling is not a problem, it is Go’s strength.

the proposal reads: after a year long discussion we are adding this built-in. use it if you want less verbose code, otherwise continue doing what you do. the reaction is some not fully justified resistance for a opt-in feature for which team members have shown clear advantages!

Yours opt-in at writing time is a must for all readers, including future-you.

clear advantages

If muddying control flow can be named ‘an advantage’, then yes.

try, for the sake of java and C++ expats’ habits, introduces magic that needs to be understood by all Gophers. In meantime sparing a minority few lines to write in a few places (as tryhard runs have shown).

I’d argue that my way simpler onErr macro would spare more lines writing, and for the majority:

x, err = fa()
onErr break

r, err := fb(x)
onErr return 0, nil, err

if r, err := fc(x); onErr && triesleft > 0 {
  triesleft--
  continue retry
}

(note that I am in the ‘leave if err!= nil alone’ camp and above counter proposal was published to show a simpler solution that can make more whiners happy.)

Edit:

i would further encourage the go team to make try a variadic built-in if that’s easy to do try(outf.Seek(linkstart, 0)), io.Copy(outf, exef)))

~Short to write, long to read, prone to slips or misunderstandings, flaky and dangerous at maintenance stage.~

I was wrong. Actually the variadic try would be much better than nests, as we might write it by lines:

try( outf.Seek(linkstart, 0),
 io.Copy(outf, exef),
)

and have try(…) return after the first err.

I ran tryhard on my codebase with 54K LOC, 1116 instances were found. I saw the diff, and I have to say that I have so very little construct that would greatly benefit from try, because almost my entire use of if err != nil type of construct is a simple single-level block that just returns the error with added context. I think I only found a couple of instances where try would actually change the construct of the code.

In other words, my take is that try in its current form gives me:

  • less typing (a reduction of whopping ~30 chars per occurrence, denoted by the “**”'s below)
-       **if err := **json.NewEncoder(&buf).Encode(in)**; err != nil {**
-               **return err**
-       **}**
+       try(json.NewEncoder(&buf).Encode(in))

while it introduces these problems for me:

  • Yet Another Way To Handle Errors
  • missing visual cue for execution path split

As I wrote earlier in this thread, I can live with try, but after trying it out on my code I think I’d personally rather not have this introduced to the language. my $.02

I just ran tryhard on a package (with vendor) and it reported 2478 with the code count dropping from 873934 to 851178 but I’m not sure how to interpret that because I don’t know how much of that is due to over-wrapping (with the stdlib lacking support for stack-trace error wrapping) or how much of that code is even about error handling.

What I do know, however, is that just this week alone I wasted an embarrassing amount of time due to copy-pasta like if err != nil { return nil } and errors that look like error: cannot process ....file: cannot parse ...file: cannot open ...file.

<rant> I wouldn’t put too much weight on the number of votes unless you think that there’s only ~3000 Go developers out there. The high vote counts on the other non-proposal is simply due to the fact that the issue made it to the top of HN and Reddit — the Go community isn’t exactly known for its lack of dogma and/or nay-saying so no-one should be surprised about the vote counts.

I also wouldn’t take the attempts at appeal-to-authority too seriously either, because these same authorities are known to reject new ideas and proposals even after their own ignorance and/or misunderstanding is pointed out. </rant>

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.

@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.

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.

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 }.

@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() {
        case w.Close:
            os.Remove(dst) // only if a “try” fails
            fallthrough
        case os.Open, os.Create, io.Copy:
            err = fmt.Errorf("copy %s %s: %v", src, dst, err)
        default:
            err = fmt.Errorf("an unexpected error occurred")
        }
    }

    return err
}

The language changes required would be:

  1. Allow a for error{} construct, similar to for range{} but only entered upon an error and only executed once.

  2. Allow omitting the capturing of return values that implement <object>.Error() string but only when a for error{} construct exists within the same func.

  3. Cause program control flow to jump to the first line of the for error{} construct when an func returns an “error” in its last return value.

  4. When returning an “error” Go would add assign a reference to the func that returned the error which should be retrievable by <error>.Source()

What is an “error”?

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()
}

A side benefits of allowing the non-capturing of “errors”

Note that allowing the non-capturing of the last “error” from func calls would allow later refactoring to return errors from funcs 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 funcs.

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.

@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.

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.

@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.

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.

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.

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.

This issue is already so long that locking it seems pointless.

Everyone, please be aware that this issue is closed, and the comments you make here will almost certainly be ignored forever. If that is OK with you, comment away.

@ianlancetaylor

Thanks for the quote from Steve Klabnik. While I appreciate and agree with the sentiment, it is worth considering that as a language Rust seems somewhat more willing to rely on syntactic details than Go has been.

I agree in general about Rust relying more than Go on syntactic details, but I don’t think this applies to this specific discussion about error handling verbosity.

Errors are values in Rust like they are in Go. You can handle them using standard control flow, like in Go. In the first versions of Rust, it was the only way to handle errors, like in Go. Then they introduced the try! macro, which is surprisingly similar to the try built-in function proposal. They eventually added the ? operator, which is a syntactic variation and a generalization of the try! macro, but this is not necessary to demonstrate the usefulness of try, and the fact that the Rust community doesn’t regret having added it.

I’m well aware of the massive differences between Go and Rust, but on the topic of error handling verbosity, I think their experience is transposable to Go. The RFCs and discussions related to try! and ? are really worth reading. I’ve been really surprised by how similar are the issues and arguments for and against the language changes.

@griesemer Thank you and everyone else on the Go team for tirelessly listening to all the feedback and putting up with all our varied opinions.

So maybe now is a good time to bring this thread to closure and to move on to future things?

I like this proposal

All of the concerns I had (e.g. ideally it should be a keyword and not a built in) are addressed by the in-depth document

It is not 100% perfect, but it is a good enough solution that a) solves an actual problem and b) does so while considering a lot of backwards compat and other issues

Sure it does some ‘magic’ but then so does defer. The only difference is keyword vs. builtin, and the choice to avoid a keyword here makes sense.

@ccbrown Got it. In retrospect, I think your suggested relaxation should be no problem. I believe we could relax try to work with any interface type (and matching result type), for that matter, not just error, as long as the relevant test remains x != nil. Something to think about. This could be done early, or retro-actively as it would be a backward-compatible change I believe.

When reading this discussion (and discussions on Reddit), I didn’t always feel like everyone was on the same page.

Thus, I wrote a little blog post that demonstrates how try can be used: https://faiface.github.io/post/how-to-use-try/.

I tried to show multiple aspects of this proposal so that everybody can see what it can do and form a more informed (even if negative) opinion.

If I missed something important, please let me know!

@nvictor Go is a language that doesn’t like non-orthogonal features. That means that if we, in the future, figure out a better error handling solution that isn’t try, it will be much more complicated to switch (if it doesn’t get flat-out rejected because our current solution is “good enough”).

I think there’s a better solution out there than try, and I’d rather take it slow and find that solution than settle for this one.

However I wouldn’t be angry if this were added. It’s not a bad solution, I just think we may be able to figure out a better one.

please use try {} catch{} syntax, don’t build more wheels

Compiler flags should not change the spec of the language. This is much more fit for vet/lint

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.

@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 ifs suffer from

Unhandled try will likely be the most common, though.

@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 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.

@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.

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.

@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).

@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.

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 😛

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).

@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.

@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.

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:

  1. 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
}
  1. Assign values like we did in the past, but use a helper wrapf function that has the if err != nil boilerplate.
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.

switch doesn’t need an else, it has default.

@myroid I wouldn’t mind having your second example made a little bit more generic in a form of switch-else statement:

i, err := strconv.Atoi("1")
switch err != nil; err {
case io.EOF:
        println("EOF")
case io.ErrShortWrite:
        println("ErrShortWrite")
} else {
    println(i)
}

@OneOfOne respectfully, I disagree that this should be reopened. This thread has established that there are real limitations with the syntax. Perhaps you are right that this is the most “sane” proposal: but I believe that the status quo is more sane still.

I agree that if err != nil is written far too often in Go- but having a singular way to return from a function hugely improves readability. While I can generally get behind proposals that reduce boilerplate code, the cost should never be readability IMHO.

I know a lot of developers lament the “longhand” error checking in go, but honestly terseness is often at odds with readability. Go has many established patterns here and elsewhere that encourage a particular way of doing things, and, in my experience, the result is reliable code that ages well. This is critical: real-world code has to be read and understood many times throughout its lifetime, but is only ever written once. Cognitive overhead is a real cost, even for experienced developers.

Thanks, @jonbodner, for your example. I’d write that code as follows (translation errors notwithstanding):

func Handler(w http.ResponseWriter, r *http.Request) {
	statusCode, err := internalHandler(w, r)
	if err != nil {
		wrap := xerrors.Errorf("handler fail: %w", err)
		logger.With(zap.Error(wrap)).Error("error")
		http.Error(w, wrap.Error(), statusCode)
	}
}

func internalHandler(w http.ResponseWriter, r *http.Request) (statusCode int, err error) {
	ctx := r.Context()
	id := chi.URLParam(r, "id")

	// starts as bad request, then it's an internal server error after we parse inputs
	statusCode = http.StatusBadRequest
	var c Thingie
	try(unmarshalBody(r, &c))

	statusCode = http.StatusInternalServerError
	s := try(DoThing(ctx, c))
	d := try(DoThingWithResult(ctx, id, s))
	data := try(json.Marshal(detail))

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	try(w.Write(data))
	
	return
}

It uses two functions but it’s a lot shorter (29 lines vs 40 lines) - and I used nice spacing - and this code doesn’t need a defer. The defer in particular, together with the statusCode being changed on the way down and used in the defer makes the original code harder to follow than necessary. The new code, while it uses named results and a naked return (you can easily replace that with return statusCode, nil if you want) is simpler because it cleanly separates error handling from the “business logic”.

This is an appreciation comment;
thanks @griesemer for the gardening and all, that you have been doing on this issue as well as elsewhere.

@griesemer Yes, of course, all of what you’re saying has basis. The gray area, however, is not as simplistic as you’re framing it to be. Naked returns are normally treated with great caution by those of us who teach others (we, who strive to grow/promote the community). I appreciate that the stdlib has it littered throughout. But, when teaching others, explicit returns is always emphasized. Let the individual reach their own maturity to turn to the more “fanciful” approach, but encouraging it from the start would surely be fostering hard-to-read code (i.e. bad habits). This, again, is the tone-deafness I’m trying to bring to light.

Personally, I do not wish to forbid naked returns or deferred value manipulation. When they are truly suitable I am glad these capabilities are available (though, other experienced users may take a more rigid stance). Nonetheless, encouraging the application of these less common and generally fragile features in such a pervasive manner is thoroughly the opposite direction I ever imagined Go taking. Is the pronounced change in character of eschewing magic and precarious forms of indirection a purposed shift? Should we also start emphasizing the use of DICs and other hard-to-debug mechanisms?

p.s. Your time is greatly appreciated. Your team and the language has my respect, and care. I don’t wish any grief for anyone in speaking out; I hope you will hear the nature of my/our concern and try to see things from our “front-lines” perspective.

@iand

I asked this of @rsc a while ago (https://github.com/golang/go/issues/32437#issuecomment-503245958):

@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?

The answer was purposed, but uninspiring and lacking substance (https://github.com/golang/go/issues/32437#issuecomment-503295558):

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.

Additional sentiment was offered (https://github.com/golang/go/issues/32437#issuecomment-503408184):

I was surprised to find a case in which try led to clearly better code, in a way that had not been discussed before.

Eventually, I answered my own question “Is there a list of classified error handling cases?”. There will effectively be 6 modes of error handling - Manual Direct, Manual Pass-through, Manual Indirect, Automatic Direct, Automatic Pass-through, Automatic Indirect. Currently, it is only common to use 2 of those modes. The indirect modes, that have a significant amount of effort being put into their facilitation, appear strongly prohibitive to most veteran Gophers and that concern is seemingly being ignored. (https://github.com/golang/go/issues/32437#issuecomment-507332843).

Further, I suggested that automated transforms be vetted prior to transformation to try to ensure the value of the results (https://github.com/golang/go/issues/32437#issuecomment-507497656). Over time, thankfully, more of the results being offered do seem to have better retrospectives, but this still does not address the impact of the indirect methods in a sober and concerted manner. After all (in my opinion), just as users should be treated as hostile, devs should be treated as lazy.

The failing of the current approach to miss valuable candidates was also pointed out (https://github.com/golang/go/issues/32437#issuecomment-507505243).

I think it’s worth being noisy about this process being generally lacking and notably tone-deaf.

The #32968 linked above is not exactly a full counter-proposal, but it builds on my disagreement with the dangerous ability to nest that the try macro possess. Unlike #32946 this one is a serious proposal, one that I hope lacks serious flaws (its yours to see, assess and comment, of course). Excerpt:

  • The check macro is not a one-liner: it helps the most where many repetitive checks using the same expression should be performed in close proximity.
  • Its implicit version already compiles at playground.

Design constraints (met)

It is a built-in, it does not nest in a single line, it allows for way more flows than try and has no expectations about the shape of a code within. It does not encourage naked returns.

usage example

// built-in 'check' macro signature: 
func check(Condition bool) {}

check(err != nil) // explicit catch: label.
{
    ucred, err := getUserCredentials(user)
    remote, err := connectToApi(remoteUri)
    err, session, usertoken := remote.Auth(user, ucred)
    udata, err := session.getCalendar(usertoken)

  catch:               // sad path
    ucred.Clear()      // cleanup passwords
    remote.Close()     // do not leak sockets
    return nil, 0, err // dress before leaving
}
// happy path

// implicit catch: label is above last statement
check(x < 4) 
  {
    x, y = transformA(x, z)
    y, z = transformB(x, y)
    x, y = transformC(y, z)
    break // if x was < 4 after any of above
  }

Hope this helps, Enjoy!

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.

@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… 😃

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)

A lot of ways of doing handlers are being proposed, but I think they often miss two key requirements:

  1. 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?

  2. 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.

@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.

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.

func (f *http2Framer) endWrite() error {
        ...
        relay n := f.w.Write(f.wbuf)
        return checkShortWrite(n, len(f.wbuf))
}

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.

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.

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:

  1. The only thing I dislike about the proposal is the need to have a named error return parameter when 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.

  2. It’s a pity that 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.

  3. I think it would be difficult for folks to get their heads around what go try(f) or defer try(f) were doing and that it’s best therefore to just prohibit them altogether.

  4. I agree with those who think that the existing error handling techniques would look less verbose if 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.

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 trys because they are less brief. Some might see this as a downside, but I think it’s a benefit.

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.”.

At this point, I think having try{}catch{} is more readable 🙃

  1. Using named imports to go around 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”.

  2. 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.

  3. 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.

  4. 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
    }
    
  5. Again, then you don’t use 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?

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.

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.

@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.

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.

@ngrilly I agree with your comments. Those biases are very difficult to avoid. What would be very helpful is a clearer understanding of how many people avoid Go due to the verbose error handling. I’m sure the number is non-zero, but it’s difficult to measure.

That said, it’s also true that try introduced a new change in flow of control that was hard to see, and that although try was intended to help with handling errors it did not help with annotating errors.

Thanks for the quote from Steve Klabnik. While I appreciate and agree with the sentiment, it is worth considering that as a language Rust seems somewhat more willing to rely on syntactic details than Go has been.

@kaedys This closed and extremely verbose issue is definitely not the right place to discuss specific alternative syntaxes for error handling.

Just repost my comment in another issue https://github.com/golang/go/issues/32853#issuecomment-510340544

I think if we can provide another parameter funcname, that will be great, otherwise we still don’t know the error is returned by which function.

func foo() error {
    handler := func(err error, funcname string) error {
        return fmt.Errorf("%s: %v", funcname, err) // wrap something
        //return nil // or dismiss
    }

    a, b := try(bar1(), handler) 
    c, d := try(bar2(), handler) 
}

Here’s a tryhard report from my company’s 300k-line Go codebase:

Initial run:

--- stats ---
  13879 (100.0% of   13879) func declarations
   4381 ( 31.6% of   13879) func declarations returning an error
  38435 (100.0% of   38435) statements
   8028 ( 20.9% of   38435) if statements
   4496 ( 56.0% of    8028) if <err> != nil statements
    453 ( 10.1% of    4496) try candidates
      4 (  0.1% of    4496) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   3066 ( 68.2% of    4496) { return ... zero values ..., expr }
    356 (  7.9% of    4496) single statement then branch
    345 (  7.7% of    4496) complex then branch; cannot use try
     63 (  1.4% of    4496) non-empty else branch; cannot use try

We have a convention of using juju’s errgo package (https://godoc.org/github.com/juju/errgo) to mask errors and add stack trace information to them, which would prevent most rewrites from happening. That does mean that we are unlikely to adopt try, for the same reason that we generally eschew naked error returns.

Since it seems like it might be a helpful metric, I removed errgo.Mask() calls (which return the error without annotation) and re-ran tryhard. This is an estimate of how many error checks could be rewritten if we didn’t use errgo:

--- stats ---
  13879 (100.0% of   13879) func declarations
   4381 ( 31.6% of   13879) func declarations returning an error
  38435 (100.0% of   38435) statements
   8028 ( 20.9% of   38435) if statements
   4496 ( 56.0% of    8028) if <err> != nil statements
   3114 ( 69.3% of    4496) try candidates
      7 (  0.2% of    4496) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
    381 (  8.5% of    4496) { return ... zero values ..., expr }
    358 (  8.0% of    4496) single statement then branch
    345 (  7.7% of    4496) complex then branch; cannot use try
     63 (  1.4% of    4496) non-empty else branch; cannot use try

So, I guess ~70% of error returns would otherwise be compatible with try.

Lastly, my primary concern with the proposal does not seem to be captured in any of the comments I read nor the discussion summaries:

This proposal significantly increases the relative cost of annotating errors.

Presently, the marginal cost of adding some context to an error is very low; it’s barely more than typing the format string. If this proposal were adopted, I worry that engineers would increasingly prefer the aesthetic offered by try, both because it makes their code “look more sleek” (which I’m sad to say is a consideration for some folks, in my experience), and now requires an additional block to add context. They could justify it based on a “readability” argument, how adding context expands the method by another 3 lines and distracts the reader from the main point. I think that corporate code bases are unlike the Go standard library in the sense that making it easy to do the right thing likely has a measurable impact on the resulting code quality, code reviews are of varying quality, and team practices vary independently of each other. Anyway, as you said before, we could always not adopt try for our codebase.

Thanks for the consideration

If anyone wants to try out try in a slightly more hands on way, I’ve created a WASM playground here with a prototype implementation:

https://ccbrown.github.io/wasm-go-playground/experimental/try-builtin/

And if anyone is actually interested in compiling code locally with try, I have a Go fork with what I believe is a fully functional / up-to-date implementation here: https://github.com/ccbrown/go/pull/1

I’m just gonna throw this out there at current stage. I will think about it some more, but I thought I post here to see what you think. Maybe I should open a new issue for this? I also posted this on #32811

So, what about doing some kind of generic C macro kind of thing instead to open up for more flexibility?

Like this:

define returnIf(err error, desc string, args ...interface{}) {
	if (err != nil) {
		return fmt.Errorf("%s: %s: %+v", desc, err, args)
	}
}

func CopyFile(src, dst string) error {
	r, err := os.Open(src)
	:returnIf(err, "Error opening src", src)
	defer r.Close()

	w, err := os.Create(dst)
	:returnIf(err, "Error Creating dst", dst)
	defer w.Close()

	...
}

Essentially returnIf will be replaced/inlined by that defined above. The flexibility there is that it’s up to you what it does. Debugging this might be abit odd, unless the editor replaces it in the editor in some nice way. This also make it less magical, as you can clearly read the define. And also, this enables you to have one line that could potentially return on error. And able to have different error messages depending on where it happened (context).

Edit: Also added colon in front of the macro to suggest that maybe that can be done to clarify it’s a macro and not a function call.

baffled and feel bad for the go team lately. try is a clean and understandable solution to the specific problem it is trying to solve: verbosity in error handling.

the proposal reads: after a year long discussion we are adding this built-in. use it if you want less verbose code, otherwise continue doing what you do. the reaction is some not fully justified resistance for a opt-in feature for which team members have shown clear advantages!

i would further encourage the go team to make try a variadic built-in if that’s easy to do

try(outf.Seek(linkstart, 0))
try(io.Copy(outf, exef))

becomes

try(outf.Seek(linkstart, 0)), io.Copy(outf, exef)))

the next verbose thing could be those successive calls to try.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 gives you a pretty good idea of what that might look like.

Thanks for doing this exercise. However, this confirms to me what I suspected, the go source code itself has a lot of places where try() would be useful because the error is just passed on. However, as I can see from the experiments with tryhard which others and myself submitted above, for many other codebases try() would not be very useful because in application code errors tend to be actually handled, not just passed on.

I would interpret this differently.

We have not had generics, so it will be hard to find code in the wild that would directly benefit from generics based on code written. That doesn’t mean that generics would not be useful.

For me, there are 2 patterns I have used in code for error handling

  1. use panics within the package, and recover the panic and return an error result in the few exported methods
  2. selectively use a defer’ed handler in some methods so I can decorate errors with rich stack file/line number PC information and more context

These patterns are not widespread but they work. 1) is used in the standard library in its unexported functions and 2) is used extensively in my codebase for the last few years because I thought it was a nice way of using the orthogonal features to do simplified error decoration, and the proposal recommends and has blessed the approach. The fact that they are not widespread doesn’t mean that they are not good. But as with everything, guidelines from the Go team recommending it will lead to them being used more in practice, in the future.

One final point of note is that decorating errors in every line of your code can be a bit too much. There will be some places where it makes sense to decorate errors, and some places where it doesn’t. Because we didn’t have great guidelines before, folks decided that it made sense to always decorate errors. But it may not add much value to always decorate each time a file didn’t open, as it may be sufficient within the package to just have an error as “unable to open file: conf.json”, as opposed to: “unable to get user name: unable to get db connection: unable to load system file: unable to open file: conf.json”.

With the combination of the error values and the concise error handling, we are now getting better guidelines on how to handle errors. The preference seems to be:

  • an error will be simple e.g. “unable to open file: conf.json”
  • an error frame can be attached that includes the context: GetUserName --> GetConnection --> LoadSystemFile.
  • If it adds to the context, you can Wrap that error somewhat e.g. MyAppError{error}

I tend to feel like we keep overlooking the goals of the try proposal, and the high level things it tries to solve:

  1. reduce boilerplate of if err != nil { return err } for places where it makes sense to propagate the error up for it to be handled higher up the stack
  2. Allow the simplified use of return values where err == nil
  3. allow for the solution to be extended later on to allow, for example, more error decoration on site, jump to error handler, use goto instead of return semantics etc.
  4. Allow error handling to not clutter the logic of the codebase i.e. put it to the side somewhat with an error handler of sorts.

Many folks still have 1). Many folks have worked around 1) because better guidelines didn’t exist before. But that doesn’t mean that, after they start using it, their negative reaction would not change to become more positive.

Many folks can use 2). There may be disagreement on how much, but I gave an example where it makes my code much easier.

var u user = try(db.LoadUser(try(strconv.ParseInt(stringId)))

In java where exceptions are the norm, we would have:

User u = db.LoadUser(Integer.parseInt(stringId)))

Nobody would look at this code and say that we have to do it in 2 lines ie.

int id = Integer.parseInt(stringId)
User u = db.LoadUser(id))

We shouldn’t have to do that here, under the guideline that try MUST not be called inline and MUST always be on its own line.

Furthermore, today, most code will do things like:

var u user
var err error
var id int
id, err = strconv.ParseInt(stringId)
if err != nil {
  return u, errors.Wrap("cannot load userid from string: %s: %v", stringId, err)
}
u, err = db.LoadUser(id)
if err != nil {
  return u, errors.Wrap("cannot load user given user id: %d: %v", id, err)
}
// now work with u

Now, someone reading this has to parse through these 10 lines, which in java would have been 1 line, and which could be 1 line with the proposal here. I visually have to mentally try to see what lines in here are really pertinent when I read this code. The boilerplate makes this code harder to read and grok.

I remember in my past life working on/with aspect oriented programming in java. There, the goal was to

This allows behaviors that are not central to the business logic (such as logging) to be added to a program without cluttering the code, core to the functionality. (quoting from wikipedia https://en.wikipedia.org/wiki/Aspect-oriented_programming ). Error handling is not central to the business logic, but is central to correctness. The idea is the same - we shouldn’t clutter our code with things not central to the business logic because “but error handling is very important”. Yes it is, and yes we can put it to the side.

Regarding 4), Many proposals have suggested error handlers, which is code to the side that handles errors but doesn’t clutter the business logic. The initial proposal has the handle keyword for it, and folks have suggested other things. This proposal says that we can leverage the defer mechanism for it, and just make that faster which was its achilles heel before. I know - I have made noise about the defer mechanism performance many times to the go team.

Note that tryhard will not flag this code as something that can be simplified. But with try and new guidelines, folks may want to simplify this code to a 1-liner and let the Error Frame capture the required context.

The context, which has been used very well in exception based languages, will capture that one tried to an error occured loading a user because the user id didn’t exist, or because the stringId was not in a format that an integer id could be parsed from it.

Combine that with Error Formatter, and we can now richly inspect the error frame and the error itself and format the message nicely for users, without the hard to read a: b: c: d: e: underlying error style that many folks have done and which we haven’t had great guidelines for.

Remember that all these proposals together give us the solution we want: concise error handling without unnecessary boilerplate, while affording better diagnostics and better error formatting for users. These are orthogonal concepts but together become extremely powerful.

Finally, given 3) above, it is hard to use a keyword to solve this. By definition, a keyword doesn’t allow extension to in the future pass a handler by name, or allow on-the-spot error decoration, or support goto semantics (instead of return semantics). With a keyword, we kinda have to have the full solution in mind first. And a keyword is not backwards compatible. The go team stated when Go 2 was starting, that they wanted to try to maintain backwards compatibility as much as possible. try function maintains that, and if we see later that there’s no extension necessary, a simple gofix can easily modify code to change try function to a keyword.

My 2 cents again!

@lestrrat I would not say my opinion in this comment but if there is a chance to explain you how “try” may affect good for us, it would be that two or more tokens can be written in if statement. So if you write 200 conditions in a if statement, you will be able to reduce many lines.

if try(foo()) == 1 && try(bar()) == 2 {
  // err
}
n1, err := foo()
if err != nil {
  // err
}
n2, err := bar()
if err != nil {
  // err
}
if n1 == 1 && n2 == 2 {
  // err
}

Do you understand that the most advantage you get in case of really bad code?

If you use unexpected() or return error as is, you know nothing about your code and your application.

try can’t help you write better code, but can produce more bad code.

I ran some experiments similar to what @lpar did on all of Heroku’s unarchived Go repositories (public and private).

The results are in this gist: https://gist.github.com/freeformz/55abbe5da61a28ab94dbb662bfc7f763

cc @davecheney

@griesemer

Continuing from https://github.com/golang/go/issues/32825#issuecomment-507120860

Going along with the premise that abuse of try will be mitigated by code review, vetting, and/or community standards, I can see the wisdom in avoiding changing the language in order to restrict the flexibility of try. I don’t see the wisdom of providing additional facilities that strongly encourage the more difficult/unpleasant to consume manifestations.

In breaking this down some, there seem to be two forms of error path control flow being expressed: Manual, and Automatic. Regarding error wrapping, there seem to be three forms being expressed: Direct, Indirect, and Pass-through. This results in six total “modes” of error handling.

Manual Direct, and Automatic Direct modes seem agreeable:

wrap := func(err error) error {
  return fmt.Errorf("failed to process %s: %v", filename, err)
}

f, err := os.Open(filename)
if err != nil {
	return nil, wrap(err)
}
defer f.Close()

info, err := f.Stat()
if err != nil {
	return nil, wrap(err)
}
// in errors, named better, and optimized
WrapfFunc := func(format string, args ...interface{}) func(error) error {
  return func(err error) error {
    if err == nil {
      return nil
    }
    s := fmt.Sprintf(format, args...)
    return errors.Errorf(s+": %w", err)
  }
}
wrap := errors.WrapfFunc("failed to process %s", filename)

f, err := os.Open(filename)
try(wrap(err))
defer f.Close()

info, err := f.Stat()
try(wrap(err))

Manual Pass-through, and Automatic Pass-through modes are also simple enough to be agreeable (despite often being a code smell):

f, err := os.Open(filename)
if err != nil {
	return nil, err
}
defer f.Close()

info, err := f.Stat()
if err != nil {
	return nil, err
}
f := try(os.Open(filename))
defer f.Close()

info := try(f.Stat())

However, Manual Indirect and Automatic Indirect modes are both quite disagreeable due to the high likelihood of subtle mistakes:

defer errd.Wrap(&err, "failed to do X for %s", filename)

var f *os.File
f, err = os.Open(filename)
if err != nil {
	return
}
defer f.Close()

var info os.FileInfo
info, err = f.Stat()
if err != nil {
	return
}
defer errd.Wrap(&err, "failed to do X for %s", filename)

f := try(os.Open(filename))
defer f.Close()

info := try(f.Stat())

Again, I can understand not forbidding them, but facilitating/blessing the indirect modes is where this is still raising clear red flags for me. Enough so, at this time, for me to remain emphatically skeptical of the entire premise.

@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.

@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 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.

@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 defers? 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.

@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:

image

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.

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)

@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.

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)
}

@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.

@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.)

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.)

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’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.

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.

Thinking further about the conditional return briefly mentioned at https://github.com/golang/go/issues/32437#issuecomment-498947603. It seems return if f, err := os.Open("/my/file/path"); err != nil 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:

-       return if f, err := os.Open("my/path") 
+       return if f, err := os.Open("my/path") {
+               fmt.Printf("DEBUG: os.Open: %s\n", err)
+       }

The meaning of the body block of return if is 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.

@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

  1. The 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.

  1. A concern from the original proposal document. I quoted it in my first comment:

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.

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.

@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.

@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.

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
}
  • Several of the greatest features in go are that current builtins ensure clear control flow, error handling is explicit and encouraged, and developers are strongly dissuaded from writing “magical” code. The try proposal is not consistent with these basic tenets, as it will promote shorthand at the cost of control-flow readability.
  • If this proposal is adopted, then perhaps consider making the 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.
  • Again, if the proposal is adopted then perhaps implement it without using 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.

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())
  ...
}
  • We can not use try() if doSomething does not have %error in return values.
  • We can not use try() if foo() does not have error in the last of 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

  • %error can be only one in the return value list of a function.

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)

It’s also very easy for everyone to write a 3-paragraph incomplete attempt for a different solution (with no real work presented on the sideline).

The only thing I (and a number of others) wanted to make try useful was an optional argument to allow it to return a wrapped version of the error instead of the unchanged error. I don’t think that needed a huge amount of design work.

Thank you @griesemer @rsc and everyone else involved with proposing. Many others have said it above, and it bears repeating that your efforts in thinking through the problem, writing the proposal, and discussing it in good faith, are appreciated. Thank you.

@pierrec

1/ Is “error handling” still an area of research?

It has been, for over 50 years! There does not seem to be an overall theory or even a practical guide for consistent and systematic error handling. In the Go land (as for other languages) there is even confusion about what an error is. For example, an EOF may be an exceptional condition when you try to read a file but why it is an error? Whether that is an actual error or not really depends on the context. And there are other such issues.

Perhaps a higher level discussion is needed (not here, though).

Maybe I’m too attached to Go, but I think a point was shown here, as Russ described: there’s a point where the community is not just a headless chicken, it is a force to be reckoned with and to be harnessed for its own good.

With due thanks to the coordination provided by the Go Team, we can all be proud that we arrived to a conclusion, one we can live with and will revisit, no doubt, when the conditions are more ripe.

Let’s hope the pain felt here will serve us well in the future (wouldn’t it be sad, otherwise?).

Lucio.

@VojtechVitek I think the points you make are subjective and can only be evaluated once people start to use it seriously.

However I believe there is one technical point that has not been discussed much. The pattern of using defer for error wrapping/decoration has performance implications beyond the simple cost of defer itself since functions that use defer cannot be inlined.

This means that adopting try with error wrapping imposes two potential costs compared with returning a wrapped error directly after an err != nil check:

  1. a defer for all paths through the function, even successful ones
  2. loss of inlining

Even though there are some impressive upcoming performance improvements for defer the cost is still non-zero.

try has a lot of potential so it would be good if the Go team could revisit the design to allow some kind of wrapping to be done at the point of failure instead of pre-emptively via defer.

@daved First, let me assure you that I/we hear you loud and clear. Though we’re still early in the process and lots of things can change. Let’s not jump the gun.

I understand (and appreciate) that one might want to chose a more conservative approach when teaching Go. Thanks.

I ran tryhard against a small internal project that I worked on over a year ago. The directory in question has the code for 3 servers (“microservices”, I suppose), a crawler that runs periodically as a cron job, and a few command-line tools. It also has fairly comprehensive unit tests. (FWIW, the various pieces have been running smoothly for over a year, and it has proved straightforward to debug and resolve any issues that arise)

Here are the stats:

--- stats ---
    370 (100.0% of     370) func declarations
    115 ( 31.1% of     370) func declarations returning an error
   1159 (100.0% of    1159) statements
    258 ( 22.3% of    1159) if statements
    123 ( 47.7% of     258) if <err> != nil statements
     64 ( 52.0% of     123) try candidates
      0 (  0.0% of     123) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     54 ( 43.9% of     123) { return ... zero values ..., expr }
      2 (  1.6% of     123) single statement then branch
      3 (  2.4% of     123) complex then branch; cannot use try
      1 (  0.8% of     123) non-empty else branch; cannot use try

Some commentary:

  1. 50% of all if statements in this codebase were doing error-checking, and try could replace ~half of those. This means a quarter of all if statements in this (small) codebase are a typed-out version of try.

  2. I should note that this is surprisingly high to me, because a few weeks before starting on this project, I happened to read of a family of internal helper functions (status.Annotate) that annotate an error message but preserve the gRPC status code. For instance, if you call an RPC and it returns an error with an associated status code of PERMISSION_DENIED, the returned error from this helper function would still have an associated status code of PERMISSION_DENIED (and theoretically, if that associated status code was propagated all the way up to an RPC handler, then the RPC would fail with that associated status code). I had resolved to use these functions for everything on this new project. But apparently, for 50% of all errors, I simply propagated an error unannotated. (Before running tryhard, I had predicted 10%).

  3. status.Annotate happens to preserve nil errors (i.e. status.Annotatef(err, "some message: %v", x) will return nil iff err == nil). I looked through all of the non-try candidates of the first category, and it seems like all would be amenable to the following rewrite:

    // Before
    enc, err := keymaster.NewEncrypter(encKeyring)                                                     
    if err != nil {                                                                                    
      return status.Annotate(err, "failed to create encrypter")                                        
    }
    
    // After
    enc, err := keymaster.NewEncrypter(encKeyring)                                                                                                                                                                  
    try(status.Annotate(err, "failed to create encrypter"))
    

    To be clear, I’m not saying this transformation is always necessarily a good idea, but it seemed worth mentioning since it boosts the count significantly to a bit under half of all if statements.

  4. defer-based error annotation seems somewhat orthogonal to try, to be honest, since it will work with and without try. But while looking through the code for this project, since I was looking closely at the error-handling, I happened to notice several instances where callee-generated errors would make more sense. As one example, I noticed several instances of code calling gRPC clients like this:

    resp, err := s.someClient.SomeMethod(ctx, req)
    if err != nil {
      return ..., status.Annotate(err, "failed to call SomeMethod")
    }
    

    This is actually a bit redundant in retrospect, since gRPC already prefixes its errors with something like "/Service.Method to [ip]:port : ".

    There was also code that called standard library functions using the same pattern:

    hreq, err := http.NewRequest("GET", targetURL, nil)
    if err != nil {
      return status.Annotate(err, "http.NewRequest failed")
    }
    

    In retrospect, this code demonstrates two issues: first, http.NewRequest isn’t calling a gRPC API, so using status.Annotate was unnecessary, and second, assuming the standard library also return errors with callee context, this particular use of error annotation was unnecessary (although I am fairly certain the standard library does not consistently follow this pattern).

In any case, I thought it was an interesting exercise to come back to this project and look carefully at how it handled errors.

One thing, @griesemer: does tryhard have the right denominator for “non-try candidates”? It looks like its using “try candidates” as the denominator, which doesn’t really make sense. Edit: answered below, I misread the stats.

@griesemer thanks for working on this and taking the time to respond to community concerns. I’m sure you’ve responded to a lot of the same questions at this point. I realize that it’s really hard to solve these problems and maintain backward compatibility at the same time. Thanks!

Thanks @nathanjsweet for your extensive comment - @ianlancetaylor already pointed out that your arguments are technically incorrect. Let me expand a bit:

  1. You mention that the part of the spec which disallows try with go and defer troubles you the most because try would be the first built-in where this is true. This is not correct. The compiler already does not permit e.g., defer append(a, 1). The same is true for other built-ins that produce a result which is then dropped on the floor. This very restriction would also apply to try for that matter (except when try doesn’t return a result). (The reason why we have even mentioned these restrictions in the design doc is to be as thorough as possible - they are truly irrelevant in practice. Also, if you read the design doc precisely, it does not say that we cannot make try work with go or defer - it simply suggests that we disallow it; mostly as a practical measure. It’s a “large ask” - to use your words - to make try work with go and defer even though it’s practically useless.)

  2. You suggest that some people find try “aesthetically revolting” is because it is not technically a function, and then you concentrate on the special rules for the signature. Consider new, make, append, unsafe.Offsetof: they all have specialized rules that we cannot express with an ordinary Go function. Look at unsafe.Offsetof which has exactly the kind of syntactic requirement for its argument (it must be a struct field!) that we require of the argument for try (it must be a single value of type error or a function call returning an error as last result). We do not express those signatures formally in the spec, for none of these built-ins because they don’t fit into the existing formalism - if they would, they wouldn’t have to be built-ins. Instead we express their rules in prose. That is why they are built-ins which are the escape hatch in Go, by design, from day one. Note also that the design doc is very explicit about this.

  3. The proposal also does address what happens when try is called with arguments (more than one): It’s not permitted. The design doc states explicitly that try accepts an (one) incoming argument expression.

  4. You are stating that “this proposal breaks the semantic meaning of builtin functions”. Nowhere does Go restrict what a built-in can do and what it cannot do. We have complete freedom here.

Thanks.

I know a lot of people have weighed in, but I would like to add my critique of the specification as is.

The part of the spec that most troubles me are these two requests:

Therefore we suggest to disallow try as the called function in a go statement. … Therefore we suggest to disallow try as the called function in a defer statement as well.

This would be the first builtin function of which this is true (you can even defer and go a panic) edit because the result didn’t need to be discarded. Creating a new builtin function that requires the compiler to give special control flow consideration seems like a large ask and breaks the semantic coherence of go. Every other control flow token in go is not a function.

A counter-argument to my complaint is that being able to defer and go a panic is probably an accident and not very useful. However my point is that the semantic coherence of functions in go is broken by this proposal not that it is important that defer and go always make sense to use. There are probably lots of non-builtin functions that would never make sense to use defer or go with, but there is no explicit reason, semantically, why they can’t be. Why does this builtin get to exempt itself from the semantic contract of funtions in go?

I know @griesemer doesn’t want aesthetic opinions about this proposal injected into the discussion, but I do think one reason folks are finding this proposal aesthetically revolting is that they can sense it doesn’t quite add up as a function.

The proposal says:

We propose to add a new function-like built-in called try with signature (pseudo-code)

func try(expr) (T1, T2, … Tn)

Except this isn’t a function (which the proposal basically admits). It is, effectively, a one-off macro built into the language spec (if it were to be accepted). There are a few of problems with this signature.

  1. What does it mean for a function to accept a generic expression as an argument, not to mention a called expression. Every other time the word “expression” is used in the spec it means something like an uncalled function. How is it that a “called” function can be thought of as being an expression, when in every other context its return values are what is semantically active. I.E. we think of a called function as being its return values. The exceptions, tellingly, are go and defer, which are both raw tokens not builtin functions.

  2. Also this proposal gets its own function signature incorrect, or at least it doesn’t make sense, the actual signature is:

func try(R1, R2, ... Rn) ((R|T)1, (R|T)2, ... (R|T)(n-1), ?Rn) 
// where T is the return params of the function that try is being called from
// where `R` is a return value from a function, `Rn` must be an error
// try will return the R values if Rn is nil and not return Tn at all
// if Rn is not nil then the T values will be returned as well as Rn at the end 
  1. The proposal doesn’t include what happens in situations where try is called with arguments. What happens if try is called with arguments:
try(arg1, arg2,..., err)

I think the reason this isn’t addressed, is because try is trying to accept an expr argument which actually represents n number of return arguments from a function plus something else, further illustrative of the fact that this proposal breaks the semantic coherence of what a function is.

My final complain against this proposal is that it further breaks the semantic meaning of builtin functions. I am not indifferent to the idea that builtin functions sometimes need to be exempt from the semantic rules of “normal” functions (like not being able to assign them to variables, etc), but this proposal creates a large set of exemptions from the “normal” rules that seem to govern functions inside golang.

This proposal effectively makes try a new thing that go hasn’t had, it’s not quite a token and it’s not quite a function, it’s both, which seems like a poor precedent to set in terms of creating semantic coherence throughout the language.

If we are going to add a new control-flow thing-ey I argue that it makes more sense to make it a raw token like goto, et al. I know we aren’t supposed to hawk proposals in this discussion, but by way of brief example, I think something like this makes a lot more sense:

f, err := os.Open("/dev/stdout")
throw err

While this does add an extra line of code I think it addresses every issue I raised, and also eliminates the whole “alternate” function signatures deficiency with try.

edit1: note about exceptions to the defer and go cases where builtin can’t be used, because results will be disregarded, whereas with try it can’t even really be said that the function has results.

I have read as much as I can to gain an understanding of this thread. I am in favor of leaving things exactly as they are.

My reasons:

  1. I, and no one I have taught Go has ever not understood error handling
  2. I find myself never skipping an error trap because it’s so easy to just do it then and there

Also, perhaps I misunderstand the proposal, but usually, the try construct in other languages results in multiple lines of code that all may potentially generate an error, and so they require error types. Adding complexity and often some sort of upfront error architecture and design effort.

In those cases (and I have done this myself), multiple try blocks are added. which lengthens code, and overshadows implementation.

If the Go implementation of try differs from that of other languages, then even more confusion will arise.

My suggestion is to leave error handling the way it is

Is this kind of violation of the “Go prefers less features” and “adding features to Go would not make it better but bigger”? I am not sure…

I just wanna say, personally I am perfectly satisfied with the old way

if err != nil {
	return …, err
}

And definitely I do not want to read the code written by others using the try… The reason can be two folds:

  1. it is sometimes hard to guess what’s inside at the first glance
  2. trys can be nested, i.e., try( ... try( ... try ( ... ) ... ) ... ), hard to read

If you think that writing code in the old fashion for passing errors is tedious, why not just copy and paste since they are always doing the same job?

Well, you might think that, we do not always want to do the same job, but then you will have to write your “handler” function. So perhaps you lose nothing if you still write in the old way.

Some people have commented that it’s not fair to count vendored code in tryhard results. For instance, in the std library vendored code includes the generated syscall packages which contain a lot of error checking and which may distort the overall picture. The newest version of tryhard now excludes file paths containing "vendor" by default (this can also be controlled with the new -ignore flag). Applied to the std library at tip:

tryhard $HOME/go/src
/Users/gri/go/src/cmd/go/testdata/src/badpkg/x.go:1:1: expected 'package', found pkg
/Users/gri/go/src/cmd/go/testdata/src/notest/hello.go:6:1: expected declaration, found Hello
/Users/gri/go/src/cmd/go/testdata/src/syntaxerror/x_test.go:3:11: expected identifier
--- stats ---
  45424 (100.0% of   45424) func declarations
   8346 ( 18.4% of   45424) func declarations returning an error
  71401 (100.0% of   71401) statements
  16666 ( 23.3% of   71401) if statements
   4812 ( 28.9% of   16666) if <err> != nil statements
     86 (  1.8% of    4812) <err> name is different from "err" (-l flag lists details)
   3463 ( 72.0% of    4812) return ..., <err> blocks in if <err> != nil statements
   1349 ( 28.0% of    4812) complex error handler in if <err> != nil statements; cannot use try (-l flag lists details)
     17 (  0.4% of    4812) non-empty else blocks in if <err> != nil statements; cannot use try (-l flag lists details)
   3345 ( 69.5% of    4812) try candidates (-l flag lists details)

Now 29% (28.9%) of all if statements appear to be for error checking (so slightly more than before), and of those about 70% appear to be candidates for try (a bit fewer than before).

And I forgot to say: I participated in your survey and I voted for better error handling, not this.

I meant I would like to see stricter impossible to forget error processing.

@cespare Fantastic report!

The fully reduced snippet is generally better, but the parenthesis are even worse than I have expected, and the try within the loop is as bad as I have expected.

A keyword is far more readable and it’s a bit surreal that that is a point many others differ on. The following is readable and does not make me concerned about subtleties due to only one value being returned (though, it still could come up in longer functions and/or those with much nesting):

func (req *Request) Decode(r Reader) (err error) {
	defer func() { err = wrapEOF(err) }()

	req.Type = try readByte(r)
	req.Body = try readString(r)
	req.ID = try readID(r)

	n := try binary.ReadUvarint(r)
	req.SubIDs = make([]ID, n)
	for i := range req.SubIDs {
		req.SubIDs[i], err = readID(r)
		try err
	}
	return nil
}

*Being fair about it, code highlighting would help a lot, but that just seems like cheap lipstick.

@griesemer in case you are looking for more data points. I’ve ran tryhard against two of our micro-services, with these results:

cloc v 1.82 / tryhard 13280 Go code lines / 148 identified for try (1%)

Another service: 9768 Go code lines / 50 identified for try (0.5%)

Subsequently tryhard inspected a wider set of various micro-services:

314343 Go code lines / 1563 identified for try (0.5%)

Doing a quick inspection. The types of packages that try could optimise are typically adapters/service wrappers that transparently return the (GRPC) error returned from the wrapped service.

Hope this helps.

Shouldn’t this be done on source that has been vetted by experienced Gophers to ensure that the replacements are rational? How much of that “2%” rewrite should have been rewritten with explicit handling? If we do not know that, then LOC remains a relatively useless metric.

*Which is exactly why my post earlier this morning focused on “modes” of error handling. It’s easier and more substantive to discuss the modes of error handling try facilitates and then wrestle with potential hazards of the code we are likely to write than it is to run a rather arbitrary line counter.

@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 what try does.

This is a really nice way to put it. Thanks.

@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.

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).

@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):

  • ternary: “If we allow it, people will nest it and the result will be a lot of bad code” ignoring that some people write better code with them, vs.
  • 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

@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!

Syntax

This discussion has identified six different syntaxes to write the same semantics from the proposal:

(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.

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.

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.

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.

It seems like the discussion has gotten focused enough that we’re now circling around a series of well-defined and -discussed tradeoffs. This is heartening, at least to me, since compromise is very much in the spirit of this language.

@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).

@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.

@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?

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.

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.

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.)

  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.

@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.

@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.

@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.

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 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!).

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.

why not just handle the case of an error that isn’t assigned to a variable.

  • remove need for named returns, compiler can do this all on its own.
  • allows for adding context.
  • handles the common use case.
  • backwards compatible
  • doesn’t interact oddly w/ defer, loops or switches.

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

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)
	}
}

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())
}

@griesemer Thanks for the clarification about the rewrite. I’m glad that it will compile.

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.

@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.

@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.


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.

@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.

It would be nice If we could use try for a block of codes alongside with the current way of handling errors. Something like this:

// Generic Error Handler
handler := func(err error) error {
    return fmt.Errorf("We encounter an error: %v", err)  
}
a := "not Integer"
b := "not Integer"

try(handler){
    f := os.Open(filename)
    x := strconv.Atoi(a)
    y, err := strconv.Atoi(b) // <------ If you want a specific error handler
    if err != nil {
        panic("We cannot covert b to int")   
    }
}

The code above seems cleaner than the initial comment. I wish I could purpose this.

I made a new proposal #35179

I think I’m glad about the decision to not go further with this. To me this felt like a quick hack to solve a small issue regarding if err != nil being on multiple lines. We don’t want to bloat Go with minor keywords to solve minor things like this do we? This is why the proposal with hygienic macros https://github.com/golang/go/issues/32620 feels better. It tries to be a more generic solution to open up more flexibility with more things. Syntax and usage discussion ongoing there, so don’t just think if it being C/C++ macros. The point there is to discuss a better way to do macros. With it, you could implement your own try.

I’m afraid there is a self-selection bias.

I’d like to coin a new term here and now: “creator bias”. If someone is willing to put the work, they should be given the benefit of the doubt.

It’s very easy for the peanut gallery to shout loud and wide on unrelated forums how they dislike a proposed solution to a problem. It’s also very easy for everyone to write a 3-paragraph incomplete attempt for a different solution (with no real work presented on the sideline). If one agrees with the status quo, ok. Fair point. Presenting anything else as a solution without a complete prposal gives you -10k points.

I agree in general about Rust relying more than Go on syntactic details, but I don’t think this applies to this specific discussion about error handling verbosity.

Since others are still adding their two cents, I guess there is still room for me to do the same.

Though I have been programming since 1987, I have only been working with Go for about a year. Back about 18 months ago when I was looking for a new language to meet certain needs I looked at both Go and Rust. I decided on Go because I felt Go code was much easier to learn and use, and that Go code was far more readable because Go seems to prefer words to convey meaning instead of terse symbols.

So I for one would be very unhappy to see Go become more Rust-like, including the use of exclamation points (!) and question marks (?) to imply meaning.

In a similar vein, I think the introduction of macros would change the nature of Go and would result in thousands of dialects of Go as is effectively the case with Ruby. So I hope macros never get added Go, either, although my guess is there is little chance of that happening, fortunately IMO.

#jmtcw

awesome,quite helpful

Excellent!

Okay… I liked this proposal but I love the way the community and Go team reacted and engaged in a constructive discussion, even though it was sometimes a bit rough.

I have 2 questions though regarding this outcome: 1/ Is “error handling” still an area of research? 2/ Do defer improvements get reprioritized?

I was just pondering over a piece of my own code, and how that’d look with try:

slurp, err := ioutil.ReadFile(path)
if err != nil {
	return err
}
return ioutil.WriteFile(path, append(copyrightText, slurp...), 0666)

Could become:

return ioutil.WriteFile(path, append(copyrightText, try(ioutil.ReadFile(path))...), 0666)

I’m not sure if this is better. It seems to make code much more difficult to read. But it might be just a matter of getting used to it.

@sylr Thanks, but we are not soliciting alternative proposals on this thread. See also this on staying focussed.

Regarding your comment: Go is a pragmatic language - using a built-in here is a pragmatic choice. It has several advantages over using a keyword as explained at length in the design doc. Note that try is simply syntactic sugar for a common pattern (in contrast to go which implements a major feature of Go and cannot be implemented with other Go mechanisms), like append, copy, etc. Using a built-in is a fine choice.

(But as I have said before, if that is the only thing that prevents try from being acceptable, we can consider making it a keyword.)

How about use bang(!) instead of try function. This could make functions chain possible:

func foo() {
	f := os.Open!("")
	defer f.Close()
	// etc
}

func bar() {
	count := mustErr!().Read!()
}

Is nobody bothered by this type of use of try:

data := try(json.Marshal(detail))

Regardless of the fact that the marshaling error can result in finding the correct line in the written code, I just feel uncomfortable knowing that this is a naked error being returned with no line number/caller information being included. Knowing the source file, function name, and line number is usually what I include when handling errors. Maybe I am misunderstanding something though.

Here are the statistics for a small GCP helper tool to automate user and project creation:

$ tryhard -r .
--- stats ---
    129 (100.0% of     129) functions (function literals are ignored)
     75 ( 58.1% of     129) functions returning an error
    725 (100.0% of     725) statements in functions returning an error
    164 ( 22.6% of     725) if statements
     93 ( 56.7% of     164) if <err> != nil statements
     64 ( 68.8% of      93) try candidates
      0 (  0.0% of      93) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     17 ( 18.3% of      93) { return ... zero values ..., expr }
      7 (  7.5% of      93) single statement then branch
      1 (  1.1% of      93) complex then branch; cannot use try
      0 (  0.0% of      93) non-empty else branch; cannot use try

After this, I went ahead and checked all the places in the code that are still dealing with an err variable to see if I could find any meaningful patterns.

Collecting errs

In a couple of places, we do’t want to stop execution on the first error and instead be able to see all the errors that occurred once at the end of the run. Maybe there is a different way of doing this that integrates well with try or some form of support for multi-errors is added to Go itself.

var errs []error
for _, p := range toDelete {
    fmt.Println("delete:", p.ProjectID)
    if err := s.DeleteProject(ctx, p.ProjectID); err != nil {
        errs = append(errs, err)
    }
}

Error Decoration Responsibility

After having read this comment again, there were suddenly a lot of potential try cases that jumped to my attention. They are all similar in that the calling function is decorating the error of a called function with information that the called function could already have added to the error:

func run() error {
    key := "MY_ENV_VAR"
    client, err := ClientFromEnvironment(key)
    if err != nil {
        // "github.com/pkg/errors"
        return errors.Wrap(err, key)
    }
    // do something with `client`
}

func ClientFromEnvironment(key string) (*http.Client, error) {
    filename, ok := os.LookupEnv(key)
    if !ok {
        return nil, errors.New("environment variable not set")
    }
    return ClientFromFile(filename)
}

Quoting the important part from the Go blog here again here for clarity:

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.” The error returned by our Sqrt is missing information about the invalid argument.

With this in mind, the above code now becomes:

func run() error {
    key := "MY_ENV_VAR"
    client := try(ClientFromEnvironment(key))
    // do something with `client`
}

func ClientFromEnvironment(key string) (*http.Client, error) {
    filename, ok := os.LookupEnv(key)
    if !ok {
        return nil, fmt.Errorf("environment variable not set: %s", key)
    }
    return ClientFromFile(filename)
}

At first glance, this seems like a minor change but in my estimation, it could mean that try is actually incentivising to push better and more consistent error handling up the function chain and closer to the source or package.

Final Notes

Overall, I think the value that try is bringing long-term is higher than the potential issues that I currently see with it, which are:

  1. A keyword might “feel” better as try is changing control flow.
  2. Using try means you can no longer put a debug stopper in the return err case.

Since those concerns are already known to the Go team, I am curious to see how these will play out in “the real world”. Thanks for your time in reading and responding to all of our messages.

Update

Fixed a function signature that didn’t return an error before. Thank you @magical for spotting that!

@balasanjay (and @lootch): Per your comment here, yes, the program https://play.golang.org/p/KenN56iNVg7 will print 1.

Since try is only concerning itself with the error result, it leaves everything else alone. It could set other return values to their zero values, but it’s not obviously clear why that would be better. For one it could cause more work when result values are named because they may have to be set to zero; yet the caller is (likely) going to ignore them if there was an error. But this is a design decision that could be changed if there are good reasons for it.

[edit: Note that this question (of whether to clear non-error results upon encountering an error) is not specific to the try proposal. Any of the proposed alternatives that don’t require an explicit return will have to answer the same question.]

Regarding your example of a writer n += try(wrappedWriter.Write(...)): Yes, in a situation where you need to increment n even in case of an error, one cannot use try - even if try does not zero non-error result values. That is because try only returns anything if there is no error: try behaves cleanly like a function (but a function that may not return to the caller, but to the caller’s caller). See the use of temporaries in the implementation of try.

But in cases like your example one also would have to be careful with an if statement and make sure to incorporate the returned byte count into n.

But perhaps I am misunderstanding your concern.

@neild Even if VolumeCreate would decorate the errors, we would still need CreateDockerVolume to add its decoration, as VolumeCreate may be called from various other functions, and if something fails (and hopefully logged) you would like to know what failed - which in this case is CreateDockerVolume , Nevertheless, Considering VolumeCreate is a part of the APIclient interface.

The same goes for other libraries - os.Open can well decorate the file name, reason for error etc, but func ReadConfigFile(...
func WriteDataFile(... etc - calling os.Open are the actual failing parts you would like to see in order to log, trace, and handle your errors - especially, but not only in production env.

@daved

the parenthesis are even worse than I have expected […] A keyword is far more readable and it’s a bit surreal that that is a point many others differ on.

Choosing between a keyword and a built-in function is mostly an aesthetic and syntactic issue. I honestly don’t understand why this is so important to your eyes.

PS: The built-in function has the advantage of being backward compatible, being extensible with other parameters in the future, and avoiding the issues around operator precedence. The keyword has the advantage of… being a keyword, and signaling try is “special”.

@mattn that’s the thing though, theoretically you are absolutely correct. I’m sure we can come up with cases where try would fit just wonderfully.

I just supplied data that in real life, at least I found almost no occurrence of such constructs that would benefit from the translation to try in my code.

It’s possible that I write code differently from the rest of the World, but I just thought it worth it for somebody to chime in that, based on PoC translation, that some of us don’t actually gain much from the introduction of try into the language.

As an aside, I still wouldn’t use your style in my code. I’d write it as

n1 := try(foo())
n2 := try(bar())
if n1 == 1 && n2 == 2 {
   return errors.New(`boo`)
}

so I still would be saving about the same amount of typing per instance of those n1/n2/…n(n)s

  • I ran tryhard against a large Go API which I maintain with a team of four other engineers full time. In 45580 lines of Go code, tryhard identified 301 errors to rewrite (so, it would be a +301/-903 change), or would rewrite about 2% of the code assuming each error takes approximately 3 lines. Taking into account comments, whitespace, imports, etc. that feels substantial to me.
  • I’ve been using tryhard’s line tool to explore how try would change my work, and subjectively it flows very nicely to me! The verb try feels clearer to me that something could go wrong in the calling function, and accomplishes it compactly. I’m very used to writing if err != nil, and I don’t really mind, but wouldn’t mind changing, either. Writing and refactoring the empty variable preceding the error (i.e. making the empty slice/map/variable to return) repetitively is probably a more tedious than the err itself.
  • It’s a little hard to follow all the threads of discussion, but I’m curious what this means for wrapping errors. It’d be nice if try was variadic if you wanted to optionally add context like try(json.Unmarshal(b, &accountBalance), "failed to decode bank account info for user %s", user). Edit: this point probably off topic; from looking at non-try rewrites this is where this happens, though.
  • I really appreciate the thought and care being put into this! Backwards compatibility and stability are really important to us and the Go 2 effort to date has been really smooth for maintaining projects. Thanks!

@sirkon Since try is special, the language could disallow nested try’s if that’s important - even if try looks like a function. Again, if this is the only road block for try, that could be easily addressed in various ways (go vet, or language restriction). Let’s move on from this - we’ve heard it many times now. Thanks.

Yep, try is the way to go. I’ve tried to add try once, and I liked it. Patch - https://github.com/ascheglov/go/pull/1 Topic on Reddit - https://www.reddit.com/r/golang/comments/6vt3el/the_try_keyword_proofofconcept/

Thanks for your note, @ugorji.

I had one question about try as an implicit goto if err != nil.

If we decide that is the direction, will it be hard to retrofit “try does a return” to “try does a goto”, given that goto has defined semantics that you cannot go past unallocated variables?

Yes, exactly right. There is some discussion on #26058. I think ‘try-goto’ has at least three strikes against it: (1) you have to answer unallocated variables, (2) you lose stack information about which try failed, which in contrast you can still capture in the return+defer case, and (3) everyone loves to hate on goto.

I was planning on writing this, and just wanted to complete it before it is locked down.

I hope the go team doesn’t see the criticism and feel that it is indicative of the majority sentiment. There’s always the tendency for the vocal minority to overwhelm the conversation, and I feel like that might have happened here. When everyone is going on a tangent, it discourages others that just want to talk about the proposal AS IS.

So - I will like to articulate my positive position for what it’s worth.

I have code that already uses defer for decorating/annotating errors, even for spitting out stack traces, exactly this reason.

See: https://github.com/ugorji/go-ndb/blob/master/ndb/ndb.go#L331 https://github.com/ugorji/go-serverapp/blob/master/app/baseapp.go#L129 https://github.com/ugorji/go-serverapp/blob/master/app/webrouter.go#L180

which all call errorutil.OnError(*error)

https://github.com/ugorji/go-common/blob/master/errorutil/errors.go#L193

This is along the lines of the defer helpers which Russ/Robert mention earlier.

It is a pattern that I already use, FWIW. It’s not magic. It’s completely go-like IMHO.

I also use it with named parameters, and it works excellently.

I say this to dispute the notion that anything recommended here is magic.

Secondly, I wanted to add some comments on try(…) as a function. It has one clear advantage over a keyword, in that it can be extended to take parameters.

There are 2 extension modes that have been discussed here:

  • extend try to take a label to jump to
  • extend try to take a handler of the form func(error) error

For each of them, it is needed that try as a function take a single parameter, and it can be extended later on to take a second parameter if necessary.

The decision has not been made on whether extending try is necessary, and if so, what direction to take. Consequently, the first direction is to provide try to eliminate most of the “if err != nil { return err }” stutter which I have loathed forever but took as the cost of doing business in go.

I personally am glad that try is a function, that I can call inline e.g. I can write

var u User = db.loadUser(try(strconv.Atoi(stringId)))

AS opposed to:

var id int // i have to define this on its own if err is already defined in an enclosing block
id, err = strconv.Atoi(stringId)
if err != nil {
  return
}
var u User = db.loadUser(id)

As you can see, I just took 6 lines down to 1. And 5 of those lines are truly boilerplate. This is something I have dealt with many times, and I have written a lot of go code and packages - you can check my github to see some of the ones I have posted online, or my go-codec library.

Finally, a lot of the comments in here haven’t truly shown problems with the proposal, as much as they have posited their own preferred way to solving the problem.

I personally am thrilled that try(…) is coming in. And I appreciate the reasons why try as a function is the preferred solution. I clearly like that defer is being used here, as it only just makes sense.

Let’s remember one of go’s core principles - orthogonal concepts that can be combined well. This proposal leverages a bunch of go’s orthogonal concepts (defer, named return parameters, built-in functions to do what is not possible via user code, etc) to provide the key benefit that go users have universally requested for years i.e. reducing/eliminating the if err != nil { return err } boilerplate. The Go User Surveys show that this is a real issue. The go team is aware that it is a real issue. I am glad that the loud voices of a few are not skewing the position of the go team too much.

I had one question about try as an implicit goto if err != nil.

If we decide that is the direction, will it be hard to retrofit “try does a return” to “try does a goto”, given that goto has defined semantics that you cannot go past unallocated variables?

@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.

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.”

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.

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.

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.)

@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.

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.

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) ?

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.

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.

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.

After reading through everything here, and upon further reflection, I am not sure I see try even as a statement something worth adding.

  1. 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 <function-call>” syntax beautifully captured starting a concurrent thread. I don’t get that sort of “aha!” feeling here. It doesn’t feel right. The cost/benefit ratio is not large enough.

  2. 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.

  3. 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”.

  4. and yet I believe a better way may be possible. Picking try now will close that option off.

@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?

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?

@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:

  1. 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")
}
  1. 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…

@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 returns 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.

@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.

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:

  • Documentation of result parameters
  • Manipulating a result value (usually an error) inside a defer

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:

  1. The deferred call to f is called with the last result parameter of g as the first input parameter of f
  2. f is only called if that error is not nil
  3. The result of f 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
	}()

	// ...
}

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.

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.

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)))))

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.

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.

@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 != nils, 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.

@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()?

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()
}

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].

  1. https://ziglang.org/documentation/master/#try

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 interesting. As written, though, it moves fmt-style formatting from a package into the language itself, which opens up a can of worms.

In case someone hate the try word which let them think of the Java, C* language, I advice not to use ‘try’ but other words like ‘help’ or ‘must’ or ‘checkError’… (ignore me)

I don’t support or am against try, but I trust Go Teams’ judgement on the matter, so far their judgement has provided with an excellent language, so I think whatever they decide will work for me, try or no try, I consider we need to understand as outsiders, that the maintainers have broader visibility over the matter. syntax we can discuss all day. I’d like to thank everyone who has worked on or is trying to improve go at the moment for their efforts, we are thankful and look forward for new (non-backwards-breaking) improvements in the language libraries and Runtime if any is deemed useful by you guys.

As a supporter of this proposal I’m naturally disappointed that it’s now been withdrawn though I think the Go team has done the right thing in the circumstances.

One thing that now seems quite clear is that the majority of Go users don’t regard the verbosity of error handling as a problem and I think that’s something the rest of us will just have to live with even if it does put off potential new users.

I’ve lost count of how many alternative proposals I’ve read and, whilst some are quite good, I haven’t seen any that I thought were worth adopting if try were to bite the dust. So the chance of some middle ground proposal now emerging seems remote to me.

On a more positive note, the current discussion has pointed out ways in which all potential errors in a function can be decorated in the same manner and in the same place (using defer or even goto) which I hadn’t previously considered and I do hope the Go team will at least consider changing go fmt to allow single statement if’s to be written on one line which will at least make error handling look more compact even if it doesn’t actually remove any boilerplate.

@pierrec I think we need a clearer understanding of what changes in error handling would be useful. Some of the error values changes will be in the upcoming 1.13 release (https://tip.golang.org/doc/go1.13#errors), and we will gain experience with them. In the course of this discussion we have seen many many many syntactical error handling proposals, and it would be helpful if people could vote and comment on any that seem particularly useful. More generally, as @griesemer said, experience reports would be helpful.

It would also be useful to better understand to what extent error handling syntax is problematic for people new to the language, though that will be hard to determine.

There is active work on improving defer performance in https://golang.org/cl/183677, and unless some major obstacle is encountered I would expect that to make it into the 1.14 release.

@jonbodner

This works (https://play.golang.org/p/NaBZe-QShpu):

package main

import (
	"errors"
	"fmt"

	"golang.org/x/xerrors"
)

func main() {
	var err error
	defer func() {
		filterCheck(recover())
		if err != nil {
			wrap := xerrors.Errorf("app fail (at count %d): %w", ct, err)
			fmt.Println(wrap)
		}
	}()

	check(retNoErr())

	n, err := intNoErr()
	check(err)

	n, err = intErr()
	check(err)

	check(retNoErr())

	check(retErr())

	fmt.Println(n)
}

func check(err error) {
	if err != nil {
		panic(struct{}{})
	}
}

func filterCheck(r interface{}) {
	if r != nil {
		if _, ok := r.(struct{}); !ok {
			panic(r)
		}
	}
}

var ct int

func intNoErr() (int, error) {
	ct++
	return 0, nil
}

func retNoErr() error {
	ct++
	return nil
}

func intErr() (int, error) {
	ct++
	return 0, errors.New("oops")
}

func retErr() error {
	ct++
	return errors.New("oops")
}

Ah, the first downvote! Good. Let the pragmatism flow through you.

The more i think the more i like this proposal. The only things that disturb me is to use named return everywhere. Is it finally a good practice and i should use it (never tried) ?

Anyway, before changing all my code, will it works like that ?

func f() error {
  var err error
  defer errd.Wrap(&err,...)
  try(...)
}

@iand Indeed - that was just an oversight of mine. My apologies.

We do believe that try does allow us to write more readable code - and much of the evidence we have received from real code and our own experiments with tryhard show significant cleanups. But readability is more subjective and harder to quantify.

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.

@griesemer can you elaborate on the specific metrics the team will be using to establish the success or failure of the experiment?

What about amending the proposal to be variadic instead of this weird expression argument?

That would solve a lot of problems. In the case where people wanted to just return the error the only thing that would change is the explicit variadic .... E.G.:

try(os.Open("/dev/stdout")...)

however, folks who want a more flexible situation can do something like:

f, err := os.Open("/dev/stdout")
try(WrapErrorf(err, "whatever wrap does: %v"))

One thing that this idea does is make the word try less appropriate, but it keeps backwards compatibility.

Executed tryhard on our codebase and this is what we got:

--- stats ---
  15298 (100.0% of   15298) func declarations
   3026 ( 19.8% of   15298) func declarations returning an error
  33941 (100.0% of   33941) statements
   7765 ( 22.9% of   33941) if statements
   3747 ( 48.3% of    7765) if <err> != nil statements
    131 (  3.5% of    3747) <err> name is different from "err"
   1847 ( 49.3% of    3747) return ..., <err> blocks in if <err> != nil statements
   1900 ( 50.7% of    3747) complex error handler in if <err> != nil statements; cannot use try
     19 (  0.5% of    3747) non-empty else blocks in if <err> != nil statements; cannot use try
   1789 ( 47.7% of    3747) try candidates

First, I want to clarify that because Go (before 1.13) lacks context in errors, we implemented our own error type that implements the error interface, some functions are declared as returning foo.Error instead of error, and it looks like this analyzer didn’t catch that so these results aren’t “fair”.

I was in the camp of “yes! let’s do this”, and I think it will be an interesting experiment for 1.13 or 1.14 betas, but I’m concerned by the 47.7% … try candidates”. It now means there are 2 ways of doing things, which I don’t like. However there are also 2 ways of creating a pointer (new(Foo) vs &Foo{}) as well as 2 ways of creating a slice or map with make([]Foo) and []Foo{}.

Now I’m on the camp of “let’s try this” :^) and see what the community thinks. Perhaps we will change our coding patterns to be lazy and stop adding context, but maybe that’s OK if errors get better context from the xerrors impl that’s coming anyways.

I don’t think this implicit error handle(syntax sugar) like try is good, because you can not handle multiple errors intuitively especially when you need need execute multiple functions sequentially.

I would suggest something like Elixir’s with statement: https://www.openmymind.net/Elixirs-With-Statement/

Something like this below in golang:

switch a, b, err1 := go_func_01(),
       apple, banana, err2 := go_func_02(),
       fans, dissman, err3 := go_func_03()
{
   normal_func()
else
   err1 -> handle_err1()
   err2 -> handle_err2()
   _ -> handle_other_errs()
}

@griesemer: I am suggesting that it is better to set the other return values to their zero values, because then it is clear what try will do from just inspecting the callsite. It will either a) do nothing, or b) return from the function with zero values and the argument to try.

As specified, try will retain the values of the non-error named return values, and one would therefore need to inspect the entire function to be clear on what values try is returning.

This is the same issue with a naked return (having to scan the whole function to see what value is being returned), and was presumably the reason for filing https://github.com/golang/go/issues/21291. This, to me, implies that try in a large function with named return values, would have to be discouraged under the same basis as naked returns (https://github.com/golang/go/wiki/CodeReviewComments#named-result-parameters). Instead, I suggest that try be specified to always return the zero values of the non-error argument.

func CreateDockerVolume(volName string) (string, error) {
	volume, err := VolumeCreate(volName)
	if err != nil {
		return "", errors.Wrapf(err, "unable to create docker volume %v", volName)
	}
	return volume.Name, nil
}

This is a somewhat interesting example. My first reaction when looking at it was to ask whether this would produce stuttering error strings like:

unable to create docker volume: VolumeName: could not create volume VolumeName: actual problem

The answer is that it doesn’t, because the VolumeCreate function (from a different repo) is:

func (cli *Client) VolumeCreate(ctx context.Context, options volumetypes.VolumeCreateBody) (types.Volume, error) {
        var volume types.Volume
        resp, err := cli.post(ctx, "/volumes/create", nil, options, nil)
        defer ensureReaderClosed(resp)
        if err != nil {
                return volume, err
        }
        err = json.NewDecoder(resp.body).Decode(&volume)
        return volume, err
}

In other words, the additional decoration on the error is useful because the underlying function didn’t decorate its error. That underlying function can be slightly simplified with try.

Perhaps the VolumeCreate function really should be decorating its errors. In that case, however, it’s not clear to me that the CreateDockerVolume function should add additional decoration, since it has no new information to provide.

@balasanjay Yes, wrapping errors is the case. But also we have logging, different reactions on different errors (what we should do with error variables, e.g. sql.NoRows?), readable code and so on. We write defer f.Close() immediately after opening a file to make it clear for readers. We check errors immediately for the same reason.

Most importantly, this proposal violates the rule “errors are values”. This is how Go is designed. And this proposal goes directly against the rule.

try(errors.Wrap(err, ...)) is another piece of terrible code because it contradicts both this proposal and the current Go design.

useless feature,it save typing, but not a big deal. I rather choose the old way. write more error handler make to program easy to trouble shooting.

Thanks everyone for all the feedback so far. At this point, it seems that we have identified the main benefits, concerns, and possible good and bad implications of try. To make progress, those need to be evaluated further by looking into what try would mean for actual code bases. The discussion at this point is circling around and repeating those same points.

Experience is now more valuable than continued discussion. We want to encourage people to take time to experiment with what try would look like in their own code bases and write and link experience reports on the feedback page.

To give everyone some time to breathe and experiment, we are going to pause this conversation and lock the issue for the next week and a half.

The lock will start around 1p PDT/4p EDT (in about 3h from now) to give people a chance to submit a pending post. We will reopen the issue for more discussion on July 1.

Please be assured that we have no intention to rush any new language features without taking the time to understand them well and make sure that they are solving real problems in real code. We will take the time needed to get this right, just as we have done in the past.

@olekukonko, re https://github.com/golang/go/issues/32437#issuecomment-503508478:

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())

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.)

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

@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.

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.

@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.

@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.

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.

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.)

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?

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.

@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.

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. image

(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.

@mikeschinkel Wouldn’t it be just as easy to forget to turn on the compiler option in that situation?

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

@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.

@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?

@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.

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.

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!") }

@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.

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()
}

@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.

@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 Result<T, error>, to loosely borrow from Rust’s terminology. try 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 trys 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.

@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.

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.

  1. @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).
  1. 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.

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.

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)

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 😦

@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.


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.

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.

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 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 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.

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.

Maybe someone should lock this issue? The discussion is probably better suited elsewhere.

@griesemer This example is inherent to try, you may not nest code that may fail today - you are forced to handle errors with control flow. I would like to something cleared up from https://github.com/golang/go/issues/32825#issuecomment-507099786 / https://github.com/golang/go/issues/32825#issuecomment-507136111 to which you replied https://github.com/golang/go/issues/32825#issuecomment-507358397. Later the same issue was discussed again in https://github.com/golang/go/issues/32825#issuecomment-508813236 and https://github.com/golang/go/issues/32825#issuecomment-508937177 - the last of which I state:

Glad you read my central argument against try: the implementation is not restrictive enough. I believe that either the implementation should match all the proposals usage examples that are concise and easy to read.

Or the proposal should contain examples that match the implementation so that all people considering it can be exposed to what will inevitably appear in Go code. Along with all the corner cases that we may face when troubleshooting less than ideally written software, which occurs in any language / environment. It should answer questions like what stack traces will look like with multiple nesting levels, are the locations of the errors easily recognizable? What about method values, anonymous function literals? What type of stack trace do the below produce if the line containing the calls to fn() fail?

fn := func(n int) (int, error) { ... }
return try(func() (int, error) { 
    mu.Lock()
    defer mu.Unlock()
    return try(try(fn(111111)) + try(fn(101010)) + try(func() (int, error) {
       // yea...
    })(2))
}(try(fn(1)))

I am well aware there will be lots of reasonable code written, but we are now providing a tool that has never existed before: the ability to potentially write code without clear control flow. So I want to justify why we even allow it in the first place, I never want my time wasted debugging this kind of code. Because I know I will, experience has taught me that someone will do it if you allow them. That someone is often an uninformed me.

Go provides the least possible ways for other developers and I to waste each-others time by limiting us to using the same mundane constructs. I don’t want to lose that without an overwhelming benefit. I don’t believe “because try is implemented as a function” to be an overwhelming benefit. Can you provide a reason why it is?

Having a stack trace that shows where the above fails would be useful, maybe adding a composite literal with fields that call that function into the mix? I am asking for this because I know how stack traces look today for this type of problem, Go does not provide easily digestible column information in the stack information only the hexadecimal function entry address. Several things worry me about this, such as stack trace consistency across architectures, for example consider this code:

package main
import "fmt"
func dopanic(b bool) int { if b { panic("panic") }; return 1 }
type bar struct { a, b, d int; b *bar }
func main() {
	fmt.Println(&bar{
		a: 1,
		c: 1,
		d: 1,
		b: &bar{
			a: 1,
			c: 1,
			d: dopanic(true) + dopanic(false),
		},
	})
}

Notice how the first playground fails at the left hand dopanic, the second on the right, yet both print an identical stack trace: https://play.golang.org/p/SYs1r4hBS7O https://play.golang.org/p/YMKkflcQuav

panic: panic

goroutine 1 [running]:
main.dopanic(...)
	/tmp/sandbox709874298/prog.go:7
main.main()
	/tmp/sandbox709874298/prog.go:27 +0x40

I would have expected the second one to be +0x41 or some offset after 0x40, which could be used to determine the actual call that failed within the panic. Even if we got the correct hexadecimal offsets, I won’t be able to determine where the failure occurred without additional debugging. Today this is an edge case, something people will rarely face. If you release a nestable version of try it will become the norm, as even the proposal includes a try() + try() strconv showing it’s both possible and acceptable to use try this way.

  1. Given the information above, what changes to stack traces do you plan on making (if any) so I can still tell where my code failed?

  2. Is try nesting allowed because you believe it should be? If so what do you see as the benefits for try nesting and how will you prevent abuse? I think tryhard should be adjusted to perform nested try’s where you envision it as acceptable so people can make a more informed decision about how it impacts their code, since currently we are getting only best / strictest usage examples. This will give us an idea what type of vet limitations will be imposed, as of right now you’ve said vet will be the defense against unreasonable try’s, but how will that materialize?

  3. Is try nesting because it happens to be a consequence of the implementation? If so doesn’t this seem like a very weak argument for the most notable language change since Go was released?

I think this change needs more consideration around try nesting. Each time I think about it some some new pain point emerges somewhere, I am very worried that all the potential negatives won’t emerge until it’s revealed in the wild. Nesting also provides an easy way to leak resources as mentioned in https://github.com/golang/go/issues/32825#issuecomment-506882164 that isn’t possible today. I think the “vet” story needs a much more concrete plan with examples of how it will provide feedback if it will be used as the defense against the harmful try() examples I’ve given here, or the implementation should provide compile time errors for usage outside your ideal best practices.

edit: I asked in gophers about play.golang.org architecture and someone mentioned it compiles via NaCl, so probably just a consequence / bug of that. But I could see this being a problem on other arch, I think a lot of the issues that could arise from introducing multiple returns per line just hasn’t been fully explored since most of the usages center around sane & clean single line usage.

@trende-jp

Can it not be solved with defer?

defer fmt.HandleErrorf(&err, "decoding %q", path)

Line numbers in error messages can also be solved as I’ve shown in my blog: How to use ‘try’.

@ccbrown I wonder if your example would be amenable to the same treatment as above; i.e., if it would make sense to factor code such that internal errors are wrapped once (by an enclosing function) before they go out (rather than wrapping them everywhere). It would seem to me (w/o knowing much about your system) that that would be preferable as it would centralize the error wrapping in one place rather than everywhere.

@griesemer Returning error instead to an enclosing function would make it possible to forget to categorize each error as an internal error, user error, authorization error, etc. As-is, the compiler catches that, and using try wouldn’t be worth trading those compile-time checks for run-time checks.

Ran tryhard on some of my codebases. Unfortunately, some of my packages have 0 try candidates despite being pretty large because the methods in them use a custom error implementation. For example, when building servers, I like for my business logic layer methods to only emit SanitizedErrors rather than errors to ensure at compile time that things like filesystem paths or system info don’t leak out to users in error messages.

For example, a method that uses this pattern might look something like this:

func (a *App) GetFriendsOfUser(userId model.Id) ([]*model.User, SanitizedError) {
	if user, err := a.GetUserById(userId); err != nil {
		// (*App).GetUserById returns (*model.User, SanitizedError)
		// This could be a try() candidate.
		return err
	} else if user == nil {
		return NewUserError("The specified user doesn't exist.")
	}

	friends, err := a.Store.GetFriendsOfUser(userId)
	// (*Store).GetFriendsOfUser returns ([]*model.User, error)
	// This could be a SQL error or a network error or who knows what.
	return friends, NewInternalError(err)
}

Is there any reason why we can’t relax the current proposal to work as long as the last return value of both the enclosing function and the try function expression implement error and are the same type? This would still avoid any concrete nil -> interface confusion, but it would enable try in situations like the above.

@fabstu The defer handler will work in your example just fine, both with and without try. Expanding your code with enclosing function:

func f() (err error) {
    defer func() {
       if err != nil {
          err = decorate(err, "msg") // here you can modify the result error as you please
       }
    }()
    ...
    if err := os.Setenv("GO111MODULE", "on"); err != nil {
        return err
    }
    ...
}

(note that the result err will be set by the return err; and the err used by the return is the one declared locally with the if - these are just the normal scoping rules in action).

Or, using a try, which will eliminate the need for the local err variable:

func f() (err error) {
    defer func() {
       if err != nil {
          err = decorate(err, "msg") // here you can modify the result error as you please
       }
    }()
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

And most probably, you’d want to use one of the proposed errors/errd functions:

func f() (err error) {
    defer errd.Wrap(&err, ... )
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

And if you don’t need wrapping it will just be:

func f() error {
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

Given that people are still proposing alternatives, I’d like to know in more detail what functionality it is that the broader Go community actually wants from any proposed new error handling feature.

I’ve put together a survey listing a bunch of different features, pieces of error handling functionality I’ve seen people propose. I’ve carefully omitted any proposed naming or syntax, and of course tried to make the survey neutral rather than favoring my own opinions.

If people would like to participate, here’s the link, shortened for sharing:

https://forms.gle/gaCBgxKRE4RMCz7c7

Everyone who participates should be able to see the summary results. Perhaps this might help focus the discussion?

In case you have many lines like these (from https://github.com/golang/go/issues/32437#issuecomment-509974901):

if !ok {
    return nil, fmt.Errorf("environment variable not set: %s", key)
}

You could use a helper function that only returns a non-nil error if some condition is true:

try(condErrorf(!ok, "environment variable not set: %s", key))

Once common patterns are identified, I think it will be possible to handle many of them with only a few helpers, first at the package level, and maybe eventually reaching the standard library. Tryhard is great, it is doing a wonderful job and giving lots of interesting information, but there is much more.

@griesemer FYI here are the results of running that latest version of tryhard on 233k lines of code I’ve been involved in, much of it not open source:

--- stats ---
   8760 (100.0% of    8760) functions (function literals are ignored)
   2942 ( 33.6% of    8760) functions returning an error
  22991 (100.0% of   22991) statements in functions returning an error
   5548 ( 24.1% of   22991) if statements
   2929 ( 52.8% of    5548) if <err> != nil statements
    163 (  5.6% of    2929) try candidates
      0 (  0.0% of    2929) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   2213 ( 75.6% of    2929) { return ... zero values ..., expr }
    167 (  5.7% of    2929) single statement then branch
    253 (  8.6% of    2929) complex then branch; cannot use try
     14 (  0.5% of    2929) non-empty else branch; cannot use try

Much of the code uses an idiom similar to:

 if err != nil {
     return ... zero values ..., errors.Wrap(err)
 }

It might be interesting if tryhard could identify when all such expressions in a function use an identical expression - i.e. when it might be possible to rewrite the function with a single common defer handler.

@dpinela The compiler already translates a switch statement such as yours as a sequence of if-else-if, so I don’t see a problem here. Also, the “syntax tree” that the compiler is using is not the “go/ast” syntax tree. The compiler’s internal representation allows for much more flexible code that can’t necessarily be translated back into Go.

On the subject of unexpected costs, I repost this from #32611

I see three classes of cost:

  1. the cost to the spec, which is elaborated in the design document.
  2. the cost to tooling (i.e. software revision), also explored in the design doc.
  3. the cost to the ecosystem, which the community has detailed at length above and in #32825.

Re nos. 1 & 2, the costs of try() are modest.

To oversimplify no. 3, most commenters believe try() would damage our code and/or the ecosystem of code we depend on, and thereby reduce our productivity and quality of product. This widespread, well-reasoned perception should not be disparaged as “non-factual” or “aesthetic”.

The cost to the ecosystem is far more important than the cost to the spec or to tooling.

@griesemer it’s patently unfair to claim that “three dozen vocal opponents” are the bulk of the opposition. Hundreds of people have commented here and in #32825. You told me on June 12, “I recognize that about 2/3 of the respondents are not happy with the proposal.” Since then over 2,000 people have voted on “leave err != nil alone” with 90% thumb-up.

I agree that os.Exit is the odd one out, but it has to be that way. os.Exit stops all goroutines; it wouldn’t make sense to only run the deferred functions of just the goroutine that calls os.Exit. It should either run all deferred functions, or none. And it’s much much easier to run none.

So, that is why I added colon in the macro example, so it would stick out and not look like a function call. Doesn’t have to be colon of course. It’s just an example. Also, a macro doesn’t hide anything. You just look at what the macro does, and there you go. Like if it was a function, but it will be inlined. It’s like you did a search and replace with the code piece from the macro into your functions where the macro usage was done. Naturally, if people make macros of macros and start to complicate things, well, blame yourself for making the code more complicated. 😃

@jonbodner I don’t think that adding hygienic macros would end the argument. Quite the opposite. A common criticism is that try “hides” the return. Macros would be strictly worse from this point of view, because anything would be possible in a macro. And even if Go would allow user-defined hygienic macros, we’d still have to debate if try should be a built-in macro predeclared in the universe block, or not. It would be logical for those opposed to try to be even more opposed to hygienic macros 😉

@jonbodner there is currently a proposal to add hygienic macros in Go. No proposed syntax or anything yet, however there hasn’t been much against the idea of adding hygienic macros. #32620

It’s great that there’s a proposal, but I suspect that the core Go team doesn’t intend to add macros. However, I would be happy to be wrong about this as it would end all of the arguments about changes that currently require modifications to the language core. To quote a famous puppet, “Do. Or do not. There is no try.”

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.

This is good, I think go team really should take this one. This is better than try, more clearly !!!

About the example with CreateDockerVolume https://github.com/golang/go/issues/32437#issuecomment-508199875 I found exactly the same kind of usage. In lib i wrap error with context at each error, in usage of the lib i will like to use try and add context in defer for the whole function.

I tried to mimik this by adding an error handler function at the start, it’s working fine:

func MyLib() error {
	return errors.New("Error from my lib")
}
func MyOtherLib() error {
	return errors.New("Error from my otherLib")
}

func Caller(a, b int) error {
	eh := func(err error) error {
		return fmt.Errorf("From Caller with %d and %d i found this error: %v", a, b, err)
	}

	err := MyLib()
	if err != nil {
		return eh(err)
	}

	err = MyOtherLib()
	if err != nil {
		return eh(err)
	}

	return nil
}

That will look fine and idiomatic with try+defer

func Caller(a, b int) (err error) {
	defer fmt.Errorf("From Caller with %d and %d i found this error: %v", a, b, &err)

	try(MyLib())
	try(MyOtherLib())

	return nil
}

@beoran I did some initial analysis of the Go Corpus (https://github.com/rsc/corpus). I believe tryhard in its current state could eliminate 41.7% of all err != nil checks in the corpus. If I exclude the pattern “_test.go”, this number rises to 51.1% (tryhard only operates on functions which return errors, and it tends to not find many of those in tests). Caveat, take these numbers with a grain of salt, I got the denominator (i.e. the number of places in code we perform err != nil checks) by using a hacked-up version of tryhard, and ideally we’d wait until tryhard reported these statistics itself.

Also, if tryhard were to become type-aware, it could theoretically perform transformations like this:

// Before.
a, err := foo()
if err != nil {
  return 0, nil, errors.Wrapf(err, "some message %v", b)
}

// After.
a, err := foo()
try(errors.Wrapf(err, "some message %v", b))

This takes advantage of errors.Wrap’s behavior of returning nil when the passed in error argument is nil. (github.com/pkg/errors is also not unique in this respect, the internal library I use for doing error wrapping also preserves nil errors, and would also work with this pattern, as would most error-handling libraries post-try, I imagine). The new generation of support-libraries would probably also name these propagation helpers slightly differently.

Given that this would apply to 50% of non-test err != nil checks out of the box, before any library evolution to support the pattern, it doesn’t seem like the Go compiler and runtime are unique, as you suggest.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 gives you a pretty good idea of what that might look like.

I had a thought while talking about try with a co-worker. Maybe try should only be enabled for the standard library in 1.14. @crawshaw and @jimmyfrasche both did a quick tour through some cases and gave some perspective, but actually re-writing the standard library code using try as much as possible would be valuable.

That gives the Go team time to re-write a non-trivial project using it, and the community can have an experience report on how it works out. We’d know how often it’s used, how often it needs to be paired with a defer, if it changes the readability of the code, how useful tryhard is, etc.

It’s a bit against the spirit of the standard library, allowing it to use something that regular Go code can’t, but it does give us a playground to see how try affects an existing codebase.

Apologies if someone else has thought of this already; I went through the various discussions and didn’t see a similar proposal.

@neild thanks.

I don’t want to derail this thread, but…

Perhaps the VolumeCreate function really should be decorating its errors. In that case, however, it’s not clear to me that the CreateDockerVolume function should add additional decoration,

The problem is, as the author of the CreateDockerVolume function i may not know whether the author of VolumeCreate had decorated their errors so i don’t need to decorate mine. And even if i knew that they had, they could decide to un-decorate their function at a later version. And since that change is not api changing they would release it as a patch/minor version and now my function which was dependent on their function having decorated errors does not have all the info i need. So generally i find myself decorating/wrapping even if the library I’m calling has already wrapped.

@pierrec Ok, let’s change it:

func (req *Request) Decode(r Reader) error {
        var errOne, errTwo error
	defer func() { err = unexpected(???) }()

	req.Type = try(readByte(r))
	…
}

@reusee And why is it better than this?

func (req *Request) Decode(r Reader) error {
	req.Type, err := readByte(r)
        if err != nil { return err }
        …
}

In what moment did we all decide that shortness better than readability?

@makhov

this proposal violates the rule “errors are values”

Not really. Errors are still values in this proposal. try() is just simplifying the control flow by being a shortcut for if err != nil { return ...,err }. The error type is already somehow “special” by being a built-in interface type. This proposal is just adding a built-in function that complements the error type. There is nothing extraordinary here.

Verbosity in error handling is a good thing in my opinion. In other words I don’t see a strong use case for try.

@cespare A decoder can also be a struct with an error type inside it, with the methods checking for err == nil before every operation and returning a boolean ok.

Because this is the process we use for codecs, try is absolutely useless because one can easily make a non magic, shorter, and more succinct idiom for handling errors for this specific case.

@networkimprov Agreed - similar suggestions have been brought up before. I’ll try to find some time over the next days to improve this.

@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.

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.

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

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?

@mikeschinkel I’m not the Carl you’re looking for.

@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.

@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 😃

@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.

@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.

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.

For a visual idea of try in the std library, head over to CL 182717.

Here’s an interesting tryhard false negative, from github.com/josharian/pct. I mention it here because:

  • it shows a way in which automated try detection is tricky
  • it illustrates that the visual cost of if err != nil impacts how people (me at least) structure their code, and that try can help with that

Before:

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))
}

@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 the func 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.

@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.

@magical You’re feedback has been addressed in the updated version of the detailed proposal.

@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.

@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.

@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.

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.

@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.

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.

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.

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.

On Tue, 11 Jun 2019, 20:19 Dmitriy Matrenichev, notifications@github.com wrote:

@ianlancetaylor https://github.com/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()

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/32437?email_source=notifications&email_token=ABNEY4XPURMASWKZKOBPBVDPZ7NALA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXN3VDA#issuecomment-500939404, or mute the thread https://github.com/notifications/unsubscribe-auth/ABNEY4SAFK4M5NLABF3NZO3PZ7NALANCNFSM4HTGCZ7Q .

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.

While I like the try-else statement, how about this syntax?

a, b, (err) := func() else { return err }

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.

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 "...)

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.

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. try()
  2. try(params)
  3. try(params, label)
  4. try(params, panic)

#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.

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.

@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 tryI 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.

@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.

@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.)

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:

  1. people don’t like named result parameters, which would become required in most cases
  2. it discourages adding detailed context to errors

See for example:

The rebuttal for those two objections is respectively:

  1. “we decided that [named result parameters] were ok”
  2. “No one is going to make you use try” / it’s not going to be appropriate for 100% of cases

I 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:

  1. 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
  1. When panic or deferreturn encounter this type of defer they will call the function passing the zero value for all their parameters

  2. Allow try to have more than one parameter

  3. 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:

  1. teach parser about the new kind of defer
  2. change the type checker to check that inside a function all the defers that have a function as parameter (instead of a call) also have the same signature
  3. change the type checker to check that the parameters passed to try match the signature of the functions passed to defer
  4. change the backend (?) to generate the appropriate deferproc call
  5. change the implementation of try to copy its arguments into the arguments of the deferred call when it encounters a call deferred by the new kind of defer.

@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.

@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.

@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).

@mattn, I highly doubt any signficant number of people will write code like that.

@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.

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.

@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.

@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 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.

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.

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.

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 🤔

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.

@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.

@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

@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.

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…

@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, 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.

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.

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.

The detailed proposal is now here (pending formatting improvements, to come shortly) and will hopefully answer a lot of questions.

Everyone, please be aware that this issue is closed, and the comments you make here will almost certainly be ignored forever. If that is OK with you, comment away. —@ianlancetaylor

@ianlancetaylor

What would be very helpful is a clearer understanding of how many people avoid Go due to the verbose error handling. I’m sure the number is non-zero, but it’s difficult to measure.

It’s not a “measure” per se, but this Hacker News discussion provides tens of comments from developers unhappy with Go error handling due to its verbosity (and some comments explain their reasoning and give code examples): https://news.ycombinator.com/item?id=20454966.

@griesemer, you announced the decision to decline the try proposal, in its current form, but you didn’t say what the Go team is planning to do next.

Do you still plan to address error handling verbosity, with another proposal that would solve the issues raised in this discussion (debugging prints, code coverage, better error decoration, etc.)?

@griesemer @ianlancetaylor @rsc Do you still plan to address error handling verbosity, with another proposal solving some or all of the issues raised here?

I wonder about whether the defer-optimizations will come too. I like annotating errors with it and xerrors together quite a lot and it’s too costly right now.

@trende-jp @faiface In addition to the line number, you could store the decorator string in a variable. This would let you isolate the specific function call that’s failing.

@griesemer I wasn’t planning to discuss alternatives here. The fact that everyone keeps suggesting alternatives is exactly why I think a survey to find out what people actually want would be a good idea. I just posted about it here to try to catch as many people as possible who are interested in the possibility of improving Go error handling.

@jonbodner example, and the way @griesemer rewrote it, is exactly the kind of code I have where I’d really like to use try.

@flibustenet Named result parameters by themselves are not a bad practice at all; the usual concern with named results is that they enable naked returns; i.e., one can simply write return w/o the need to specify the actual results with the return. In general (but not always!) such practice makes it harder to read and reason about the code because one can’t simply look at the return statement and conclude what the result is. One has to scan the code for the result parameters. One may miss to set a result value, and so forth. So in some code bases, naked returns are simply discouraged.

But, as I mentioned before, if the results are invalid in case of an error, it’s perfectly fine to set the error and ignore the rest. A naked return in such cases is perfectly ok as long as the error result is consistently set. try will ensure exactly that.

Finally, named result parameters are only needed if you want to augment the error return with defer. The design doc briefly also discusses the possibility to provide another built-in to access the error result. That would eliminate the need for named returns completely.

Regarding your code example: This will not work as expected because try always sets the result error (which is unnamed in this case). But you are declaring a different local variable err and the errd.Wrap operates on that one. It won’t be set by try.

@fastu And finally, you can use errors/errd also without try and then you get:

func f() (err error) {
    defer errd.Wrap(&err, ... )
    ...
    if err := os.Setenv("GO111MODULE", "on"); err != nil {
        return err
    }
    ...
}

@lpar You’re welcome to discuss alternatives but please don’t do this in this issue. This is about the try proposal. The best place would actually be one of the mailing lists, e.g. go-nuts. The issue tracker is really best for tracking and discussing a specific issue rather than a general discussion. Thanks.

func main() {
    key := "MY_ENV_VAR"
    client := try(ClientFromEnvironment(key))
    // do something with `client`
}

@mrkanister Nitpicking, but you can’t actually use try in this example because main does not return an error.

@networkimprov I can confirm that at least for gorm these are results from the latest tryhard. The “non-try candidates” are simply not reported in the tables above.

@daved Professional code is not being developed in a vacuum - there are local conventions, style recommendations, code reviews, etc. (I’ve said this before). Thus, I don’t see why abuse would be “likely” (it’s possible, but that’s true for any language construct).

Note that using defer to decorate errors is possible with or without try. There are certainly good reason for a function that contains many error checks, all of which decorate errors the same way, to do that decoration once, for instance using a defer. Or maybe use a wrapper function that does the decoration. Or any other mechanism that fits the bill and the local coding recommendations. After all, “errors are just values” and it totally makes sense to write and factor code that deals with errors.

Naked returns can be problematic when used in an undisciplined way. That doesn’t mean they are generally bad. For instance, if a function’s results are valid only if there was no error, it seems perfectly fine to use a naked return in the case of an error - as long as we are disciplined with setting the error (as the other return values don’t matter in this case). try ensures exactly that. I don’t see any “abuse” here.

@griesemer You have certainly added some substance clarifying that my (and others’) concerns might be addressed directly. My question, then, is whether the Go team does see the likely abuse of the indirect modes (i.e. naked returns and/or post-function scope error mutation via defer) as a cost worth discussing during step 5, and that it is worth potentially taking action toward it’s mitigation. The current mood is that this most disconcerting aspect of the proposal is seen as a clever/novel feature by the Go team (This concern is not addressed by the assessment of the automated transformations and seems to be actively encouraged/supported. - errd, in conversation, etc.).

edit to add… The concern with the Go team encouraging what veteran Gophers see as prohibitive is what I meant regarding tone-deafness. … Indirection is a cost that many of us are deeply concerned about as a matter of experiential pain. It may not be something that can be benchmarked easily (if at all reasonably), but it is disingenuous to regard this concern as sentimental itself. Rather, disregarding the wisdom of shared experience in favor of simple numbers without solid contextual judgment is the sort of sentiment I/we are trying to work against.

@nathanjsweet That was already considered (in fact it was an oversight) but it makes try not extensible. See https://go-review.googlesource.com/c/proposal/+/181878 .

More generally, I think you are are focussing your critique on the the wrong thing: The special rules for the try argument are really a non-issue - virtually every built-in has special rules.

@griesemer

Note also that the design doc is very explicit about this.

Can you point this out. I was surprised to read this.

You are stating that “this proposal breaks the semantic meaning of builtin functions”. Nowhere does Go restrict what a built-in can do and what it cannot do. We have complete freedom here.

I think this is a fair point. However, I do think that there is what is spelled out in the design docs and what feels like “go” (which is something Rob Pike talks about a lot). I think it is fair for me to say that the try proposal expands the ways in which builtin functions break the rules by which we expect functions to behave, and I did acknowledge that I understand why this is necessary for other builtins, but I think in this case the expansion of breaking the rules is:

  1. Counter-intuitive in some ways. This is the first function that changes control flow logic in a way that doesn’t unwind the stack (like panic and os.Exit do)
  2. A new exception to how the calling conventions of a function work. You gave the example of unsafe.Offsetof as a case where there is a syntactic requirement for a function call (it is surprising to me actually that this causes a compile-time error, but that’s another issue), but the syntactic requirement, in this case, is a different syntactic requirement than the one you stated. unsafe.Offsetof requires one argument, whereas try requires an expression that would look, in every other context, like a value returned from a function (i.e. try(os.Open("/dev/stdout"))) and could be safely assumed in every other context to return only one value (unless the expression looked like try(os.Open("/dev/stdout")...)).

@nathanjsweet Some of what you say turns out not to be the case. The language does not permit using defer or go with the pre-declared functions append cap complex imag len make new real. It also does not permit defer or go with the spec-defined functions unsafe.Alignof unsafe.Offsetof unsafe.Sizeof.

@nathanjsweet the proposal you seek is #32611 😃

@reusee I appreciate that suggestion, I didn’t realise it could be used like that. It does seem a bit grating to me though, I’m trying to put my finger on why.

I think that “try” is an odd word to use in that way. “try(action())” makes sense in english, whereas “try(value)” doesn’t really. I’d be more ok with it if it were a different word.

Also try(wrap(...)) evaluates wrap(...) first right? How much of that do you think gets optimised away by the compiler? (Compared to just running if err != nil?)

Also also #32611 is a vaguely similar proposal, and the comments have some enlightening opinions from both the core Go team and community members, in particular around the differences between keywords and builtin functions.

The principles I agree with:

  • Error handling a function call deserves its own line. Go is deliberately explicit in control flow, and I think packing that down into an expression is at odds with its explicitness.
  • It would be beneficial to have an error handling method that fits on one line. (And ideally require only one word or a few characters of boilerplate before the actual error handling). 3 lines of error handling for every function call is a friction point in the language that deserves some love and attention.
  • Any builtin that returns (like the proposed try) should at least be a statement, and ideally have the word return in it. Again, I think control flow in Go should be explicit.
  • Go’s errors are most useful when they have extra context included (I almost always add context to my errors). A solution for this problem should also support context-adding error handling code.

Syntax I support:

  • a reterr _x_ statement (syntactic sugar for if err != nil { return _x_ }, explicitly named to indicate it will return)

So the common cases could be one nice short, explicit line:

func foo() error {
    a, err := bar()
    reterr err

    b, err := baz(a)
    reterr fmt.Errorf("getting the baz of %v: %v", a, err)

    return nil
}

Instead of the 3 lines they are now:

func foo() error {
    a, err := bar()
    if err != nil {
        return err
    }

    b, err := baz()
    if err != nil {
        return fmt.Errorf("getting the baz of %v: %v", a, err)
    }

    return nil
}

Things I Disagree with:

  • “This is too small a change to be worth changing the language” I disagree, this is a quality of life change that removes the largest source of friction I have when writing Go code. When calling a function requires 4 lines
  • “It would be better to wait for a more general solution” I disagree, I think this problem is worthy of its own dedicated solution. The generalised version of this problem is reducing boilerplate code, and the generalised answer is macros - which goes against the Go ethos of explicit code. If Go isn’t going to provide a general macro facility, then it should instead provide some specific, very widely used macros like reterr (every person who writes Go would benefit from reterr).

Here was an interesting talk from Rust linked on Reddit. Most relevant part starts at 47:55

@eric-hawthorne Defers performance is a separate issue. Try doesn’t inherently require defer and doesn’t remove the ability to handle errors without it.

Isn’t the performance of defer an issue with this proposed solution? I’ve benchmarked functions with and without defer and there was significant performance impact. I just googled someone else who’s done such a benchmark and found a 16x cost. I don’t remember mine being that bad but 4x slower rings a bell. How can something that might double or worse the run time of lots of functions be considered a viable general solution?

@ngrilly there are several ways to make sure that macros stick out and are easy to see. The way Rust does it is that macros are always proceeded by ! (ie try!(...) and println!(...)).

I’d argue that if hygienic macros were adopted and easy to see, and didn’t look like normal function calls, they would fit much better. We should opt for more general-purpose solutions rather than fix individual problems.

@jonbodner there is currently a proposal to add hygienic macros in Go. No proposed syntax or anything yet, however there hasn’t been much against the idea of adding hygienic macros. #32620

I’m just gonna throw this out there at current stage. I will think about it some more, but I thought I post here to see what you think. Maybe I should open a new issue for this? I also posted this on #32811

So, what about doing some kind of generic C macro kind of thing instead to open up for more flexibility?

@Chillance , IMHO, I think that a hygienic macro system like Rust (and many other languages) would give people a chance to play with ideas like try or generics and then after experience is gained, the best ideas can become part of the language and libraries. But I also think that there’s very little chance that such a thing will be added to Go.

In my see, I want to try a block code, now try like a handle err func

I like the overall idea of reusing defer to address the problem. However, I’m wondering if try keyword is the right way to do it. What if we could reuse already existing pattern. Something that everyone already knows from imports:

Explicit handling

res, err := doSomething()
if err != nil {
    return err
}

Explicit ignoring

res, _ := doSomething()

Deferred handling

Similar behaviour to what try is going to do.

res, . := doSomething()

@griesemer

The design doc currently has the following statements:

If the enclosing function declares other named result parameters, those result parameters keep whatever value they have. If the function declares other unnamed result parameters, they assume their corresponding zero values (which is the same as keeping the value they already have).

This implies that this program would print 1, instead of 0: https://play.golang.org/p/KenN56iNVg7.

As was pointed out to me on Twitter, this makes try behave like a naked return, where the values being returned are implicit; to figure out what actual values are being returned, one might need to look at code at a significant distance from the call to try itself.

Given that this property of naked returns (non-locality) is generally disliked, what are your thoughts on having try always return the zero values of the non-error arguments (if it returns at all)?

Some considerations:

This might make some patterns involving the use of named return values unable to use try. For instance, for implementations of io.Writer, which need to return a count of bytes written, even in the partial write situation. That said, it seems like try is error-prone in this case anyways (e.g. n += try(wrappedWriter.Write(...)) does not set n to the right number in the event of an error return). It seems fine to me that try will be rendered unusable for these kinds of use-cases, as scenarios where we need both values and an error are rather rare, in my experience.

If there is a function with many uses of try, this might lead to code bloat, where there are many places in a function which need to zero-out the output variables. First, the compiler’s pretty good at optimizing out unnecessary writes these days. And second, if it proves necessary, it seems like a straightforward optimization to have all try-generated blocks goto to a common shared function-wide label, which zeroes the non-error output values.

Also, as I’m sure you’re aware, tryhard is already implemented this way, so as a side benefit this will retroactively make tryhard more correct.

@makhov You can make it more explicit:

func (req *Request) Decode(r Reader) error {
	req.Type, err := readByte(r)
        try(err) // or add annotation like try(annotate(err, ...))
	req.Body, err := readString(r)
        try(err)
	req.ID, err := readID(r)
        try(err)

	n, err := binary.ReadUvarint(r)
        try(err)
	req.SubIDs = make([]ID, n)
	for i := range req.SubIDs {
		req.SubIDs[i], err := readID(r)
                try(err)
	}
	return nil
}

@makhov I think that the snippet is incomplete as the returned error is not named (I quickly re-read the proposal but couldnt see if the variable name err is the default name if none is set).

Having to rename returned parameters is one of the point that people who reject this proposal do not like.

func (req *Request) Decode(r Reader) (err error) {
	defer func() { err = unexpected(err) }()

	req.Type = try(readByte(r))
	req.Body = try(readString(r))
	req.ID = try(readID(r))

	n := try(binary.ReadUvarint(r))
	req.SubIDs = make([]ID, n)
	for i := range req.SubIDs {
		req.SubIDs[i] = try(readID(r))
	}
	return nil
}

@ngrilly Simplifying? How?

func (req *Request) Decode(r Reader) error {
	defer func() { err = unexpected(err) }()

	req.Type = try(readByte(r))
	req.Body = try(readString(r))
	req.ID = try(readID(r))

	n := try(binary.ReadUvarint(r))
	req.SubIDs = make([]ID, n)
	for i := range req.SubIDs {
		req.SubIDs[i] = try(readID(r))
	}
	return nil
}

How I should understand that error was returned inside loop? Why it’s assigned to err var, not to foo? Is it simpler to keep it in mind and not keep it in code?

Why have a keyword (or function) at all?

If the calling context expects n+1 values, then all is as before.

If the calling context expects n values, the try behaviour kicks in.

(This is particularly useful in the case n=1 which is where all the awful clutter comes from.)

My ide already highlights ignored return values; it would be trivial to offer visual cues for this if required.

Just some thoughts…

That idiom is useful in go but it is just that: an idiom that you must teach to newcomers. A new go programmer has to learn that, otherwise they may be even tempted to refactor out the “hidden” error handling. Also, the code’s not shorter using that idiom (quite the opposite) unless you forget to count the methods.

Now let’s imagine try is implemented, how useful will that idiom be for that use case? Considering:

  • Try keeps the implementation closer rather than spread across methods.
  • Programmers will read and write code with try much more often than that specific idiom (which is seldom used except for every specific tasks). A more used idiom becomes more natural and readable unless there’s a clear disadvantage, which clearly is not the case here if we compare both with an open mind.

So maybe that idiom will be considered superseded by try.

Em ter, 2 de jul de 2019 18:06, as notifications@github.com escreveu:

@cespare https://github.com/cespare A decoder can also be a struct with an error type inside it, with the methods checking for err == nil before every operation and returning a boolean ok.

Because this is the process we use for codecs, try is absolutely useless because one can easily make a non magic, shorter, and more succinct idiom for handling errors for this specific case.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/32437?email_source=notifications&email_token=AAT5WM3YDDRZXVXOLDQXKH3P5O7L5A5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODZCRHXA#issuecomment-507843548, or mute the thread https://github.com/notifications/unsubscribe-auth/AAT5WMYXLLO74CIM6H4Y2RLP5O7L5ANCNFSM4HTGCZ7Q .

@griesemer I added a new file to the gist. It was generated with -err=“”. I spot checked and there are a few changes. I also updated tryhard this morning as well, so the newer version was also used.

We ran tryhard -err="" on our biggest (±163k lines of code including tests) service - it has found 566 occurrences. I suspect it would be even more in practice, since some of the code was written with if err != nil in mind, so it was designed around it (Rob Pike’s “errors are values” article on avoiding repeating comes to mind).

@kingishb How many of found try spots are in public functions from non-main packages? Typically public functions should return package-native (i.e. wrapped or decorated) errors…

@ubikenobi Your safer function ~is~ was leaking.

Also, I’ve never seen a value returned after an error. Though, I could imagine it making sense when a function is all about the error and the other values returned are not contingent on the error itself (maybe leading to two error returns with the second “guarding” the previous values).

Last, while not common, err == nil provides a legitimate test for some early returns.

This whole 2nd proposal thing looks very similar to digital influencers organizing a rally to me. Popularity contests do not evaluate technical merits.

People may be silent but they still expect Go 2. I personally look forward to this and the rest of Go 2. Go 1 is a great language and well suited to different kinds of programs. I hope Go 2 will expand that.

Finally I will also reverse my preference for having try as an statement. Now I support the proposal as it is. After so many years under the “Go 1” compat promise people think Go has been carved in stone. Due to that problematic assumption, not changing the language syntax in this instance seems like a much better compromise in my eyes now. Edit: I also look forward to seeing the experience reports for fact-checking.

PS: I wonder what kind of opposition will happen when generics are proposed.

Try must not be a function to avoid that damned

info := try(try(os.Open(filename)).Stat())

file leak.

I mean try statement will not allow chaining. And it is better looking as a bonus. There are compatibility issues though.

@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.

@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

  • It easy to clearly see the error
  • Least possible to mutate into the abomination the others can give birth to
  • try 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 natural
  • Errors can be very very expensive, they need as much visibility as possible and I think that is the reason go did not support the traditional try & 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.

@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?

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.

@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.

@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.

@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()).

@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.

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 !

@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?

@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") ?

“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 }

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.

@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

@griesemer One area that might be worth expanding a bit more is how transitions work in a world with try, perhaps including:

  • The cost of transitioning between error decoration styles.
  • The classes of possible mistakes that might result when transitioning between styles.
  • What classes of mistakes would be (a) caught immediately by a compiler error, vs. (b) caught by vet or staticcheck or similar, vs. © might lead to a bug that might not be noticed or would need to be caught via testing.
  • The degree to which tooling might mitigate the cost and chance of error when transitioning between styles, and in particular, whether or not gopls (or another utility) could or should have a role in automating common decoration style transitions.

Stages of error decoration

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.

Mistakes when transitioning between decoration styles

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.

Automating transitioning between decoration styles

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.

Idiomatic results?

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.

@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

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.

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

The more I think, I like the current proposal, as is.

If we need error handling, we always have the if statement.

@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>

@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.

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.

@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

@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.

@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.

@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.

@marwan-at-work, shouldn’t try(err, "someFunc failed") be try(&err, "someFunc failed") in your example?

I do not like this approach, because of:

  • try() function call interrupts code execution in the parent function.
  • there is no return keyword, but the code actually returns.

@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.

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.

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!") }

“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…)

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.

@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

@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
}

@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?

@mikeschinkel see my extension, he and i had similar ideas i just extended it with an optional block statement

@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.

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
}

@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!

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

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.

@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. 😃

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.

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…

Op vr 7 jun. 2019 01:04 schreef pj notifications@github.com:

Asper #32437 (comment) 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”?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/32437?email_source=notifications&email_token=AAARM6OOOLLYO5ZCE6VVL2TPZGJWRA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXEMYZY#issuecomment-499698791, or mute the thread https://github.com/notifications/unsubscribe-auth/AAARM6K5AOR2DES4QDTNLSTPZGJWRANCNFSM4HTGCZ7Q .

@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 like this proposal also.

And I have a request.

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

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.

Is it possible to make it work without brackets?

I.e. something like: a := try func(some)

@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 defers 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.

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.

@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

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)

@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.

@cpuguy83

I wonder if a rust style approach would be more palatable?

The proposal presents an argument against this.

@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.

@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.

@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.

@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.

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.

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.

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.

I think the ? would be a better fit than try, and always having to chase the defer for error would also be tricky.

This also closes the gates for having exceptions using try/catch forever.

@s4n-gt Thanks for this link. I was not aware of it.

@webermaster Only the last error result is special for the expression passed to try, as described in the proposal doc.