Go: proposal: Go 2: simplify error handling with || err suffix

Created on 25 Jul 2017  ·  519Comments  ·  Source: golang/go

There have been many proposals for how to simplify error handling in Go, all based on the general complaint that too much Go code contains the lines

if err != nil {
    return err
}

I'm not sure that there is a problem here to be solved, but since it keeps coming up, I'm going to put out this idea.

One of the core problems with most suggestions for simplifying error handling is that they only simplify two ways of handling errors, but there are actually three:

  1. ignore the error
  2. return the error unmodified
  3. return the error with additional contextual information

It is already easy (perhaps too easy) to ignore the error (see #20803). Many existing proposals for error handling make it easier to return the error unmodified (e.g., #16225, #18721, #21146, #21155). Few make it easier to return the error with additional information.

This proposal is loosely based on the Perl and Bourne shell languages, fertile sources of language ideas. We introduce a new kind of statement, similar to an expression statement: a call expression followed by ||. The grammar is:

PrimaryExpr Arguments "||" Expression

Similarly we introduce a new kind of assignment statement:

ExpressionList assign_op PrimaryExpr Arguments "||" Expression

Although the grammar accepts any type after the || in the non-assignment case, the only permitted type is the predeclared type error. The expression following || must have a type assignable to error. It may not be a boolean type, not even a named boolean type assignable to error. (This latter restriction is required to make this proposal backward compatible with the existing language.)

These new kinds of statement is only permitted in the body of a function that has at least one result parameter, and the type of the last result parameter must be the predeclared type error. The function being called must similarly have at least one result parameter, and the type of the last result parameter must be the predeclared type error.

When executing these statements, the call expression is evaluated as usual. If it is an assignment statement, the call results are assigned to the left-hand side operands as usual. Then the last call result, which as described above must be of type error, is compared to nil. If the last call result is not nil, a return statement is implicitly executed. If the calling function has multiple results, the zero value is returned for all result but the last one. The expression following the || is returned as the last result. As described above, the last result of the calling function must have type error, and the expression must be assignable to type error.

In the non-assignment case, the expression is evaluated in a scope in which a new variable err is introduced and set to the value of the last result of the function call. This permits the expression to easily refer to the error returned by the call. In the assignment case, the expression is evaluated in the scope of the results of the call, and thus can refer to the error directly.

That is the complete proposal.

For example, the os.Chdir function is currently

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

Under this proposal, it could be written as

func Chdir(dir string) error {
    syscall.Chdir(dir) || &PathError{"chdir", dir, err}
    return nil
}

I'm writing this proposal mainly to encourage people who want to simplify Go error handling to think about ways to make it easy to wrap context around errors, not just to return the error unmodified.

FrozenDueToAge Go2 LanguageChange NeedsInvestigation Proposal error-handling

Most helpful comment

A plain idea, with support for error decoration, but requiring a more drastic language change (obviously not for go1.10) is the introduction of a new check keyword.

It would have two forms: check A and check A, B.

Both A and B need to be error. The second form would only be used when error-decorating; people that do not need or wish to decorate their errors will use the simpler form.

1st form (check A)

check A evaluates A. If nil, it does nothing. If not nil, check acts like a return {<zero>}*, A.

Examples

  • If a function just returns an error, it can be used inline with check, so
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

becomes

check UpdateDB()
  • For a function with multiple return values, you'll need to assign, like we do now.
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

becomes

a, b, err := Foo()
check err

// use a and b

2nd form (check A, B)

check A, B evaluates A. If nil, it does nothing. If not nil, check acts like a return {<zero>}*, B.

This is for error-decorating needs. We still check on A, but is B that is used in the implicit return.

Example

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

becomes

a, err := Foo()
check err, &BarError{"Bar", err}

Notes

It's a compilation error to

  • use the check statement on things that do not evaluate to error
  • use check in a function with return values not in the form { type }*, error

The two-expr form check A, B is short-circuited. B is not evaluated if A is nil.

Notes on practicality

There's support for decorating errors, but you pay for the clunkier check A, B syntax only when you actually need to decorate errors.

For the if err != nil { return nil, nil, err } boilerplate (which is very common) check err is as brief as it could be without sacrificing clarity (see note on the syntax below).

Notes on syntax

I'd argue that this kind of syntax (check .., at the beginning of the line, similar to a return) is a good way to eliminate error checking boilerplate without hiding the control flow disruption that implicit returns introduce.

A downside of ideas like the <do-stuff> || <handle-err> and <do-stuff> catch <handle-err> above, or the a, b = foo()? proposed in another thread, is that they hide the control flow modification in a way that makes the flow harder to follow; the former with || <handle-err> machinery appended at the end of an otherwise plain-looking line, the latter with a little symbol that can appear everywhere, including in the middle and at the end of a plain-looking line of code, possibly multiple times.

A check statement will always be top-level in the current block, having the same prominence of other statements that modify the control flow (for example, an early return).

All 519 comments

    syscall.Chdir(dir) || &PathError{"chdir", dir, e}

Where does e come from there? Typo?

Or did you mean:

func Chdir(dir string) (e error) {
    syscall.Chdir(dir) || &PathError{"chdir", dir, e}
    return nil
}

(That is, does the implicit err != nil check first assign error to the result parameter, which can be named to modify it again before the implicit return?)

Sigh, messed up my own example. Now fixed: the e should be err. The proposal puts err in scope to hold the error value of the function call when not in an assignment statement.

While I'm not sure if I agree with the idea or the syntax, I have to give you credit for giving attention to adding context to errors before returning them.

This might be of interest to @davecheney, who wrote https://github.com/pkg/errors.

What happens in this code:

if foo, err := thing.Nope() || &PathError{"chdir", dir, err}; err == nil || ignoreError {
}

(My apologies if this isn't even possible w/o the || &PathError{"chdir", dir, e} part; I'm trying to express that this feels like a confusing override of existing behavior, and implicit returns are... sneaky?)

@object88 I would be fine with not permitting this new case in a SimpleStmt as used in if and for and switch statements. That would probably be best though it would complicate the grammar slightly.

But if we don't do that, then what happens is that if thing.Nope() returns a non-nil error, then the calling function returns with &PathError{"chdir", dir, err} (where err is the variable set by the call to thing.Nope()). If thing.Nope() returns a nil error, then we know for sure that err == nil is true in the condition of the if statement, and so the body of the if statement is executed. The ignoreError variable is never read. There is no ambiguity or override of existing behavior here; the handling of || introduced here is only accepted when the expression after || is not a boolean value, which means that it would not compile currently.

I do agree that implicit returns are sneaky.

Yeah, my example is pretty poor. But not permitting the operation inside an if, for, or switch would resolve a lot of potential confusion.

Since the bar for consideration is generally something that is hard to do in the language as is, I decided to see how hard this variant was to encode in the language. Not much harder than the others: https://play.golang.org/p/9B3Sr7kj39

I really dislike all of these proposals for making one type of value and one position in the return arguments special. This one is in some ways actually worse because it also makes err a special name in this specific context.

Though I certainly agree that people (including me!) should be wearier of returning errors without extra context.

When there are other return values, like

if err != nil {
  return 0, nil, "", Struct{}, wrap(err)
}

it can definitely get tiresome to read. I somewhat liked @nigeltao's suggestion for return ..., err in https://github.com/golang/go/issues/19642#issuecomment-288559297

If I understand correctly, to build the syntax tree the parser would need to know the types of the variables to distinguish between

boolean := BoolFunc() || BoolExpr

and

err := FuncReturningError() || Expr

It doesn't look good.

less is more...

When the return ExpressionList contains two ore more elements, how it works?

BTW, I want panicIf instead.

err := doSomeThing()
panicIf(err)

err = doAnotherThing()
panicIf(err)

@ianlancetaylor In your proposal's example err is still not declared explicitly, and pulled in as 'magical' (language predefined), right?

Or will it be something like

func Chdir(dir string) error {
    return (err := syscall.Chdir(dir)) || &PathError{"chdir", dir, err}
}

?

On the other hand (since it is marked as a "language change" already...)
Introduce a new operator (!! or ??) that does the shortcut on error != nil (or any nullable?)

func DirCh(dir string) (string, error) {
    return dir, (err := syscall.Chdir(dir)) !! &PathError{"chdir", dir, err}
}

Sorry if this is too far off :)

I agree that error handling in Go can be repetitive. I don't mind the repetition, but too many of them affects readability. There is a reason why "Cyclomatic Complexity" (whether you believe in it or not) uses control flows as a measure for complexity. The "if" statement adds extra noise.

However, the proposed syntax "||" is not very intuitive to read, especially since the symbol is commonly known as an OR operator. In addition, how do you deal with functions that return multiple values and error?

I am just tossing some ideas here. How about instead of using error as output, we use error as input? Example: https://play.golang.org/p/rtfoCIMGAb

Thanks for all the comments.

@opennota Good point. It could still work but I agree that aspect is awkward.

@mattn I don't think there is a return ExpressionList, so I'm not sure what you are asking. If the calling function has multiple results, all but the last are returned as the zero value of the type.

@mattn panicif does not address one the key elements of this proposal, which is an easy way to return an error with additional context. And, of course, one can write panicif today easily enough.

@tandr Yes, err is defined magically, which is pretty awful. Another possibility would be to permit the error-expression to use error to refer to the error, which is awful in a different way.

@tandr We could use a different operator but I don't see any big advantage. It doesn't seem to make the result any more readable.

@henryas I think the proposal explains how it handles multiple results.

@henryas Thanks for the example. The thing I dislike about that kind of approach is that it makes the error handling the most prominent aspect of the code. I want the error handling to be present and visible but I don't want it to be the first thing on the line. That is true today, with the if err != nil idiom and the indentation of the error handling code, and it should remain true if any new features are added for error handling.

Thanks again.

@ianlancetaylor I don't know if you checked out my playground link but it had a "panicIf" that let you add extra context.

I'll reproduce a somewhat simplified version here:

func panicIf(err error, transforms ...func(error) error) {
  if err == nil {
    return
  }
  for _, transform := range transforms {
    err = transform(err)
  }
  panic(err)
}

Coincidentally, I just gave a lightning talk at GopherCon where I used (but didn't seriously propose) a bit of syntax to aid in visualizing error-handling code. The idea is to put that code to the side to get it out of the way of the main flow, without resorting to any magic tricks to shorten the code. The result looks like

func DirCh(dir string) (string, error) {
    dir := syscall.Chdir(dir)        =: err; if err != nil { return "", err }
}

where =: is the new bit of syntax, a mirror of := that assigns in the other direction. Obviously we'd also need something for = as well, which is admittedly problematic. But the general idea is to make it easier for the reader to understand the happy path, without losing information.

On the other hand, the current way of error handling does have some merits in that it serves as a glaring reminder that you may be doing too many things in a single function, and some refactoring may be overdue.

I really like the the syntax proposed by @billyh here

func Chdir(dir string) error {
    e := syscall.Chdir(dir) catch: &PathError{"chdir", dir, e}
    return nil
}

or more complex example using https://github.com/pkg/errors

func parse(input io.Reader) (*point, error) {
    var p point

    err := read(&p.Longitude) catch: nil, errors.Wrap(err, "Failed to read longitude")
    err = read(&p.Latitude) catch: nil, errors.Wrap(err, "Failed to read Latitude")
    err = read(&p.Distance) catch: nil, errors.Wrap(err, "Failed to read Distance")
    err = read(&p.ElevationGain) catch: nil, errors.Wrap(err, "Failed to read ElevationGain")
    err = read(&p.ElevationLoss) catch: nil, errors.Wrap(err, "Failed to read ElevationLoss")

    return &p, nil
}

A plain idea, with support for error decoration, but requiring a more drastic language change (obviously not for go1.10) is the introduction of a new check keyword.

It would have two forms: check A and check A, B.

Both A and B need to be error. The second form would only be used when error-decorating; people that do not need or wish to decorate their errors will use the simpler form.

1st form (check A)

check A evaluates A. If nil, it does nothing. If not nil, check acts like a return {<zero>}*, A.

Examples

  • If a function just returns an error, it can be used inline with check, so
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

becomes

check UpdateDB()
  • For a function with multiple return values, you'll need to assign, like we do now.
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

becomes

a, b, err := Foo()
check err

// use a and b

2nd form (check A, B)

check A, B evaluates A. If nil, it does nothing. If not nil, check acts like a return {<zero>}*, B.

This is for error-decorating needs. We still check on A, but is B that is used in the implicit return.

Example

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

becomes

a, err := Foo()
check err, &BarError{"Bar", err}

Notes

It's a compilation error to

  • use the check statement on things that do not evaluate to error
  • use check in a function with return values not in the form { type }*, error

The two-expr form check A, B is short-circuited. B is not evaluated if A is nil.

Notes on practicality

There's support for decorating errors, but you pay for the clunkier check A, B syntax only when you actually need to decorate errors.

For the if err != nil { return nil, nil, err } boilerplate (which is very common) check err is as brief as it could be without sacrificing clarity (see note on the syntax below).

Notes on syntax

I'd argue that this kind of syntax (check .., at the beginning of the line, similar to a return) is a good way to eliminate error checking boilerplate without hiding the control flow disruption that implicit returns introduce.

A downside of ideas like the <do-stuff> || <handle-err> and <do-stuff> catch <handle-err> above, or the a, b = foo()? proposed in another thread, is that they hide the control flow modification in a way that makes the flow harder to follow; the former with || <handle-err> machinery appended at the end of an otherwise plain-looking line, the latter with a little symbol that can appear everywhere, including in the middle and at the end of a plain-looking line of code, possibly multiple times.

A check statement will always be top-level in the current block, having the same prominence of other statements that modify the control flow (for example, an early return).

@ALTree, I didn't understand how your example:

a, b, err := Foo()
check err

Achieves the three valued return from the original:

return "", "", err

Is it just returning zero values for all the declared returns except the final error? What about cases where you'd like to return a valid value along with an error, e.g. number of bytes written when a Write() fails?

Whatever solution we go with should minimally restrict the generality of error handling.

Regarding the value of having check at the start of the line, my personal preference is to see the primary control flow at the start of each line and have the error handling interfere with the readability of that primary control flow as little as possible. Also, if the error handling is set apart by a reserved word like check or catch, then pretty much any modern editor is going to highlight the reserved word syntax in some way and make it noticeable even if it's on the right hand side.

@billyh this is explained above, on the line that says:

If not nil, check acts like a return {<zero>}*, A

check will return the zero value of any return value, except the error (in last position).

What about cases where you'd like to return a valid value along with an error

Then you'll use the if err != nil { idiom.

There are many cases where you'll need a more sophisticate error-recovering procedure. For example you may need, after catching an error, to roll back something, or to write something on a log file. In all these cases, you'll still have the usual if err idiom in your toolbox, and you can use it to start a new block, where any kind of error-handling related operation, no matter how articulated, can be carried out.

Whatever solution we go with should minimally restrict the generality of error handling.

See my answer above. You'll still have if and anything else the language gives you now.

pretty much any modern editor is going to highlight the reserved word

Maybe. But introducing opaque syntax, that requires syntax highlighting to be readable, is not ideal.

this particular bug can be fixed by introducing a double return facility to the language.
in this case the function a() returns 123:

func a() int {
b()
return 456
}
func b() {
return return int(123)
}

This facility can be used to simplify error handling as follows:

func handle(var *foo, err error )(var *foo, err error ) {
if err != nil {
return return nil, err
}
return var, nil
}

func client_code() (*client_object, error) {
var obj, err =handle(something_that_can_fail())
// this is only reached if something not failed
// otherwise the client_code function would propagate the error up the stack
assert(err == nil)
}

This allows people to write an error handler functions that can propagate the errors up the stack
such error handling functions can be separate from main code

Sorry if I got it wrong, but I want to clarify a point, the function below will produce an error, vet warning or will it be accepted?

func Chdir(dir string) (err error) {
    syscall.Chdir(dir) || err
    return nil
}

@rodcorsi Under this proposal your example would be accepted with no vet warning. It would be equivalent to

if err := syscall.Chdir(dir); err != nil {
    return err
}

How about expanding the use of Context to handle error? For instance, given the following definition:
type ErrorContext interface { HasError() bool SetError(msg string) Error() string }
Now in the error-prone function ...
func MyFunction(number int, ctx ErrorContext) int { if ctx.HasError() { return 0 } return number + 1 }
In the intermediate function ...
func MyIntermediateFunction(ctx ErrorContext) int { if ctx.HasError() { return 0 } number := 0 number = MyFunction(number, ctx) number = MyFunction(number, ctx) number = MyFunction(number, ctx) return number }
And in the upper level function
func main() { ctx := context.New() no := MyIntermediateFunction(ctx) if ctx.HasError() { log.Fatalf("Error: %s", ctx.Error()) return } fmt.Printf("%d\n", no) }
There are several benefits using this approach. First, it doesn't distract the reader from the main execution path. There is minimal "if" statements to indicate deviation from the main execution path.

Second, it doesn't hide error. It is clear from the method signature that if it accepts ErrorContext, then the function may have errors. Inside the function, it uses the normal branching statements (eg. "if") which shows how error is handled using normal Go code.

Third, error is automatically bubbled up to the interested party, which in this case is the context owner. Should there be an additional error processing, it will be clearly shown. For instance, let's make some changes to the intermediate function to wrap any existing error:
func MyIntermediateFunction(ctx ErrorContext) int { if ctx.HasError() { return 0 } number := 0 number = MyFunction(number, ctx) number = MyFunction(number, ctx) number = MyFunction(number, ctx) if ctx.HasError() { ctx.SetError(fmt.Sprintf("wrap msg: %s", ctx.Error()) return } number *= 20 number = MyFunction(number, ctx) return number }
Basically, you just write the error-handling code as needed. You don't need to manually bubble them up.

Lastly, you as the function writer have a say whether the error should be handled. Using the current Go approach, it is easy to do this ...
````
//given the following definition
func MyFunction(number int) error

//then do this
MyFunction(8) //without checking the error
With the ErrorContext, you as the function owner can make the error checking optional with this:
func MyFunction(ctx ErrorContext) {
if ctx != nil && ctx.HasError() {
return
}
//...
}
Or make it compulsory with this:
func MyFunction(ctx ErrorContext) {
if ctx.HasError() { //will panic if ctx is nil
return
}
//...
}
If you make error handling compulsory and yet the user insists on ignoring error, they can still do that. However, they have to be very explicit about it (to prevent accidental ignore). For instance:
func UpperFunction(ctx ErrorContext) {
ignored := context.New()
MyFunction(ignored) //this one is ignored

 MyFunction(ctx) //this one is handled

}
````
This approach changes nothing to the existing language.

@ALTree Alberto, what about mixing your check and what @ianlancetaylor proposed?

so

func F() (int, string, error) {
   i, s, err := OhNo()
   if err != nil {
      return i, s, &BadStuffHappened(err, "oopsie-daisy")
   }
   // all is good
   return i+1, s+" ok", nil
}

becomes

func F() (int, string, error) {
   i, s, err := OhNo()
   check i, s, err || &BadStuffHappened(err, "oopsie-daisy")
   // all is good
   return i+1, s+" ok", nil
}

Also, we can limit check to only deal with error types, so if you need multiple return values, they need to be named and assigned, so it assigns "in-place" somehow and behaves like simple "return"

func F() (a int, s string, err error) {
   i, s, err = OhNo()
   check err |=  &BadStuffHappened(err, "oopsy-daisy")  // assigns in place and behaves like simple "return"
   // all is good
   return i+1, s+" ok", nil
}

If return would become acceptable in expression one day, then check is not needed, or becomes a standard function

func check(e error) bool {
   return e != nil
}

func F() (a int, s string, err error) {
   i, s, err = OhNo()
   check(err) || return &BadStuffHappened(err, "oopsy-daisy")
   // all is good
   return i+1, s+" ok", nil
}

the last solution feels like Perl though 😄

I can't remember who originally proposed it, but here's another syntax idea (everybody's favorite bikeshed :-). I'm not saying it's a good one, but if we're throwing ideas into the pot...

x, y := try foo()

would be equivalent to:

x, y, err := foo()
if err != nil {
    return (an appropriate number of zero values), err
}

and

x, y := try foo() catch &FooErr{E:$, S:"bad"}

would be equivalent to:

x, y, err := foo()
if err != nil {
    return (an appropriate number of zero values), &FooErr{E:err, S:"bad"}
}

The try form has certainly been proposed before, a number of times, modulo superficial syntax differences. The try ... catch form is less often proposed, but it is clearly similar to @ALTree's check A, B construct and @tandr's follow-up suggestion. One difference is that this is an expression, not a statement, so that you can say:

z(try foo() catch &FooErr{E:$, S:"bad"})

You could have multiple try/catches in a single statement:

p = try q(0) + try q(1)
a = try b(c, d() + try e(), f, try g() catch &GErr{E:$}, h()) catch $BErr{E:$}

although I don't think we want to encourage that. You'd also need to be careful here about order of evaluation. For example, whether h() is evaluated for side-effects if e() returns a non-nil error.

Obviously, new keywords like try and catch would break Go 1.x compatibility.

I suggest that we should squeeze the target of this proporsal. What issue will fixes by this proporsal? Reduce following three lines into two or one ? This might be change of language of return/if.

if err != nil {
    return err
}

Or reduce the number of times to check err? It may be try/catch solution for this.

I'd like to suggest that any reasonable shortcut syntax for error-handling have three properties:

  1. It shouldn't appear before the code it is checking, so that the non-error path is prominent.
  2. It shouldn't introduce implicit variables into the scope, so that readers don't get confused when there is an explicit variable with the same name.
  3. It shouldn't make one recovery action (for example, return err) easier than another. Sometimes an entirely different action might be preferable (like calling t.Fatal). We also don't want to discourage people from adding additional context.

Given those constraints, it seems that one nearly minimal syntax would be something like

STMT SEPARATOR_TOKEN VAR BLOCK

For example,

syscall.Chdir(dir) :: err { return err }

which is equivalent to

if err := syscall.Chdir(dir); err != nil {
    return err
}
````
Even though it's not much shorter, the new syntax moves the error path out of the way. Part of the change would be to modify `gofmt` so it doesn't line-break one-line error-handling blocks, and it indents multi-line error-handling blocks past the opening `}`.

We could make it a bit shorter by declaring the error variable in place with a special marker, like

syscall.Chdir(dir) :: { return @err }
```

I wonder how this behave as for non-zero-value and error both returned. For example, bufio.Peek possibly return non-zero value and ErrBufferFull both in same time.

@mattn you can still use the old syntax.

@nigeltao Yes, I understand. I suspect this behavior possibly make a bug on user's code since bufio.Peek also return non-zero and nil. The code must not implicit values and error both. So the value and error both should be returned to caller (in this case).

ret, err := doSomething() :: err { return err }
return ret, err

@jba What you're describing looks a bit like a transposed function-composition operator:

syscall.Chdir(dir) ⫱ func (err error) { return &PathError{"chdir", dir, err} }

But the fact that we're writing mostly-imperative code requires that we not use a function in the second position, because part of the point is to be able to return early.

So now I'm thinking about three observations that are all kind of related:

  1. Error handling is like function composition, but the way we do things in Go is sort of the opposite of Haskell's Error monad: because we're mostly writing imperative instead of sequential code, we want to transform the error (to add context) rather than the non-error value (which we'd rather just bind to a variable).

  2. Go functions that return (x, y, error) usually really mean something more like a union (#19412) of (x, y) | error.

  3. In languages that unpack or pattern-match unions, the cases are separate scopes, and a lot of the trouble that we have with errors in Go is due to unexpected shadowing of redeclared variables that might be improved by separating those scopes (#21114).

So maybe what we want really is like the =: operator, but with a sort of union-matching conditional:

syscall.Chdir(dir) =? err { return &PathError{"chdir", dir, err} }

```go
n := io.WriteString(w, s) =? err { return err }

and perhaps a boolean version for `, ok` index expressions and type assertions:
```go
y := m[x] =! { return ErrNotFound }

Except for the scoping, that's not much different from just changing gofmt to be more amenable to one-liners:

err := syscall.Chdir(dir); if err != nil { return &PathError{"chdir", dir, err} }

```go
n, err := io.WriteString(w, s); if err != nil { return err }

```go
y, ok := m[x]; if !ok { return ErrNotFound }

But the scoping is a big deal! The scoping issues are where this sort of code crosses the line from "somewhat ugly" to "subtle bugs".

@ianlancetaylor
While I'm a fan of the overall idea, I'm not a huge supporter of the cryptic perl-like syntax for it. Perhaps a more wordy syntax would be less confusing, like:

syscall.Chdir(dir) or dump(err): errors.Wrap(err, "chdir failed")

syscall.Chdir(dir) or dump

Also, I didn't understand whether the last argument is popped in case of assignment, E.g.:

resp := http.Get("https://example.com") or dump

Let's not forget that errors are values in go and not some special type.
There is nothing we can do to other structs that we can't do to errors and the other way around. This means that if you understand structs in general, you understand errors and how they are handled (even If you think it is verbose)

This syntax would require new and old developers to learn a new bit of information before they can start to understand code that uses it.

That alone makes this proposal not worth it IMHO.

Personally I prefer this syntax

err := syscall.Chdir(dir)
if err != nil {
    return err
}
return nil

over

if err := syscall.Chdir(dir); err != nil {
    return err
}
return nil

It is one line more but it separates the intended action from the error handling. This form is the most readable for me.

@bcmills:

Except for the scoping, that's not much different from just changing gofmt to be more amenable to one-liners

Not just the scoping; there is also the left edge. I think that really affects readability. I think

syscall.Chdir(dir) =: err; if err != nil { return &PathError{"chdir", dir, err} } 

is much clearer than

err := syscall.Chdir(dir); if err != nil { return &PathError{"chdir", dir, err} } 

especially when it occurs on multiple consecutive lines, because your eye can scan the left edge to ignore error handling.

Mixing the idea @bcmills we can introduce conditional pipe forwarding operator.

The F2 function will be executed if the last value is not nil.

func F1() (foo, bar){}

first := F1() ?> last: F2(first, last)

A special case of pipe forwarding with return statement

func Chdir(dir string) error {
    syscall.Chdir(dir) ?> err: return &PathError{"chdir", dir, err}
    return nil
}

Real example brought by @urandom in another issue
For me much more readable with focus in primary flow

func configureCloudinit(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) (cloudconfig.UserdataConfig, error) {
    // When bootstrapping, we only want to apt-get update/upgrade
    // and setup the SSH keys. The rest we leave to cloudinit/sshinit.
    udata := cloudconfig.NewUserdataConfig(icfg, cloudcfg) ?> err: return nil, err
    if icfg.Bootstrap != nil {
        udata.ConfigureBasic() ?> err: return nil, err
        return udata, nil
    }
    udata.Configure() ?> err: return nil, err
    return udata, nil
}

func ComposeUserData(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig, renderer renderers.ProviderRenderer) ([]byte, error) {
    if cloudcfg == nil {
        cloudcfg = cloudinit.New(icfg.Series) ?> err: return nil, errors.Trace(err)
    }
    _ = configureCloudinit(icfg, cloudcfg) ?> err: return nil, errors.Trace(err)
    operatingSystem := series.GetOSFromSeries(icfg.Series) ?> err: return nil, errors.Trace(err)
    udata := renderer.Render(cloudcfg, operatingSystem) ?> err: return nil, errors.Trace(err)
    logger.Tracef("Generated cloud init:\n%s", string(udata))
    return udata, nil
}

I agree error handling is not ergonomic. Namely, when you read below code you have to vocalize it to if error not nil then -which translates to if there is an error then.

if err != nil {
    // handle error
}

I would like to have abbility of expresing above code in such way -which in my opinion is more readable.

if err {
    // handle error
}

Just my humble suggestion :)

It does look like perl, it even has the magic variable
For reference, in perl you would do

open (FILE, $file) or die("cannot open $file: $!");

IMHO, it's not worth it, one point I like about go is that error handling
is explicit and 'in your face'

If we do stick with it, I'd like to have no magic variables, we should be
able to name the error variable

e := syscall.Chdir(dir) ?> e: &PathError{"chdir", dir, e}

And we might as well use a symbol different from || specific for this task,
I guess text symbols like 'or' are not possible due to backwards
compatibility

n, _, err, _ = somecall(...) ?> err: &PathError{"somecall", n, err}

On Tue, Aug 1, 2017 at 2:47 PM, Rodrigo notifications@github.com wrote:

Mixing the idea @bcmills https://github.com/bcmills we can introduce
conditional pipe forwarding operator.

The F2 function will be executed if the last value is not nil.

func F1() (foo, bar){}
first := F1() ?> last: F2(first, last)

A special case of pipe forwarding with return statement

func Chdir(dir string) error {
syscall.Chdir(dir) ?> err: return &PathError{"chdir", dir, err}
return nil
}

Real example
https://github.com/juju/juju/blob/01b24551ecdf20921cf620b844ef6c2948fcc9f8/cloudconfig/providerinit/providerinit.go
brought by @urandom https://github.com/urandom in another issue
For me much more readable with focus in primary flow

func configureCloudinit(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) (cloudconfig.UserdataConfig, error) {
// When bootstrapping, we only want to apt-get update/upgrade
// and setup the SSH keys. The rest we leave to cloudinit/sshinit.
udata := cloudconfig.NewUserdataConfig(icfg, cloudcfg) ?> err: return nil, err
if icfg.Bootstrap != nil {
udata.ConfigureBasic() ?> err: return nil, err
return udata, nil
}
udata.Configure() ?> err: return nil, err
return udata, nil
}
func ComposeUserData(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig, renderer renderers.ProviderRenderer) ([]byte, error) {
if cloudcfg == nil {
cloudcfg = cloudinit.New(icfg.Series) ?> err: return nil, errors.Trace(err)
}
configureCloudinit(icfg, cloudcfg) ?> err: return nil, errors.Trace(err)
operatingSystem := series.GetOSFromSeries(icfg.Series) ?> err: return nil, errors.Trace(err)
udata := renderer.Render(cloudcfg, operatingSystem) ?> err: return nil, errors.Trace(err)
logger.Tracef("Generated cloud init:\n%s", string(udata))
return udata, nil
}


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/21161#issuecomment-319359614, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AbwRO_J0h2dQHqfysf2roA866vFN4_1Jks5sTx5hgaJpZM4Oi1c-
.

Am I the only one that thinks all these proposed changes would be more complicated than the current form.

I think simplicity and brevity are not equal or interchangeable. Yes all these changes would be one or more lines shorter but would introduce operators or keywords which a user of the language would have to learn.

@rodcorsi I know it seems minor, but I think it's important for the second part to be a Block: the existing if and for statements both use blocks, and select and switch both use curly-brace-delimited syntax, so it seems awkward to omit the braces for this one particular control-flow operation.

It's also much easier to ensure that the parse tree is unambiguous if you don't have to worry about arbitrary expressions following the new symbols.

The syntax and semantics I had in mind for my sketch are:


NonZeroGuardStmt = ( Expression | IdentifierList ":=" Expression |
                     ExpressionList assign_op Expression ) "=?" [ identifier ] Block .
ZeroGuardStmt = ( Expression | IdentifierList ":=" Expression |
                  ExpressionList assign_op Expression ) "=!" Block .

A NonZeroGuardStmt executes Block if the last value of an Expression is not equal to the zero value of its type. If an identifier is present, it is bound to that value within Block. A ZeroGuardStmt executes Block if the last value of an Expression is equal to the zero value of its type.

For the := form, the other (leading) values of the Expression are bound to the IdentifierList as in a ShortVarDecl. The identifiers are declared in the containing scope, which implies that they are also visible within the Block.

For the assign_op form, each left-hand side operand must be addressable, a map index expression, or (for = assignments only) the blank identifier. Operands may be parenthesized. The other (leading) values of the right-hand side Expression are evaluated as in an Assignment. The assignment occurs prior to execution of the Block and regardless of whether the Block is then executed.


I believe the proposed grammar here is compatible with Go 1: ? is not a valid identifier and there are no existing Go operators using that character, and although ! is a valid operator, there is no existing production in which it may be followed by a {.

@bcmills LGTM, with concomitant changes to gofmt.

I would have thought you would make =? and =! each a token in its own right, which would make the grammar trivially compatible.

I would have thought you would make =? and =! each a token in its own right, which would make the grammar trivially compatible.

We can do that in the grammar, but not in the lexer: the sequence "=!" can appear in valid Go 1 code (https://play.golang.org/p/pMTtUWgBN9).

The curly-brace is what makes the parse unambiguous in my proposal: =! can currently only appear in a declaration of or assignment to a boolean variable, and declarations and assignments cannot currently appear immediately before a curly-brace (https://play.golang.org/p/ncJyg-GMuL) unless separated by an implicit semicolon (https://play.golang.org/p/lhcqBhr7Te).

@romainmenke Nope. You're not the only one. I fail to see the value of a one-liner error handling. You may save one line but add much more complexity. The problem is that in many of these proposals, the error handling part becomes hidden. The idea is not to make them less noticeable because error handling is important, but to make the code easier to read. Brevity doesn't equal to easy readability. If you have to make changes to the existing error handling system, I find the conventional try-catch-finally is much more appealing than many ideas purposes here.

I like the check proposal because you can also extend it to handle

f, err := os.Open(myfile)
check err
defer check f.Close()

Other proposals don't seem like they can mix with defer as well. check is also very readable, and simple to Google if you don't know it. I don't think it needs to be limited to the error type. Anything that's a return parameter in the last position could use it. So, a iterator might have a check for a Next() bool.

I once wrote a Scanner that looks like

func (s *Scanner) Next() bool {
    if s.Error != nil || s.pos >= s.RecordCount {
        return false
    }
    s.pos++

    var rt uint8
    if !s.read(&rt) {
        return false
    }
...

That last bit could be check s.read(&rt) instead.

@carlmjohnson

Other proposals don't seem like they can mix with defer as well.

If you're assuming that we'll expand defer to allow returning from the outer function using the new syntax, you can apply that assumption equally well to other proposals.

defer f.Close() =? err { return err }

Since @ALTree's check proposal introduces a separate statement, I don't see how you could mix that with a defer that does anything other than simply returning the error.

defer func() {
  err := f.Close()
  check err, fmt.Errorf(…, err) // But this func() doesn't return an error!
}()

Contrast:

defer f.Close() =? err { return fmt.Errorf(…, err) }

The justification for many of these proposals is better "ergonomics" but I don't really see how any of these are better other than making it so there's slightly less to type. How do these increase the maintainability of the code? The composability? The readability? The ease of understanding the control flow?

@jimmyfrasche

How do these increase the maintainability of the code? The composability? The readability? The ease of understanding the control flow?

As I noted earlier, the major advantage of any of these proposals would probably need to come from clearer scoping of assignments and err variables: see #19727, #20148, #5634, #21114, and probably others for various ways that people have encountering scoping issues in relation to error-handling.

@bcmills thanks for providing a motivation and sorry I missed it in your earlier post.

Given that premise, however, wouldn't it be better to provide a more general facility for "clearer scoping of assignments" that could be used by all variables? I've unintentionally shadowed my share of non-error variables as well, certainly.

I remember when the current behavior of := was introduced—a lot of that thread on go nuts† was clamor for a way to explicitly annotate which names to be reused instead of the implicit "reuse only if that variable exists in exactly the current scope" which is where all the subtle hard-to-see problems manifest, in my experience.

† I cannot find that thread does anyone have a link?

There are lots of things that I think could be improved about Go but the behavior of := always struck me as the one serious mistake. Maybe revisiting the behavior of := is the way to solve the root problem or at least to reduce the need for other more extreme changes?

@jimmyfrasche

Given that premise, however, wouldn't it be better to provide a more general facility for "clearer scoping of assignments" that could be used by all variables?

Yes. That's one of the things I like about the =? or :: operator that @jba and I have proposed: it also extends nicely to (an admittedly limited subset of) non-errors.

Personally, I suspect I would be happier in the long term with a more explicit tagged-union/varint/algebraic datatype feature (see also #19412), but that's a much bigger change to the language: it's difficult to see how we would retrofit that onto existing APIs in a mixed Go 1 / Go 2 environment.

The ease of understanding the control flow?

In my and @bcmills's proposals, your eye can scan down the left side and easily take in the non-error control flow.

@bcmills I think I'm responsible for at least half of the words in #19412 so you don't need to sell me on sum types ;)

When it comes to returning stuff with an error there are four cases

  1. just an error (don't need to do anything, just return an error)
  2. stuff AND an error (you'd handle that exactly as you would now)
  3. one thing OR an error (you could use sum types! :tada: )
  4. two or more things OR an error

If you hit 4 that's where things get tricky. Without introducing tuple types (unlabeled product types to go with struct's labeled product types) you'd have to reduce the problem to case 3 by bundling everything up in a struct if you want to use sum types to model "this or an error".

Introducing tuple types would cause all kinds of problems and compatibility issues and weird overlaps (is func() (int, string, error) an implicitly defined tuple or are multiple return values a separate concept? If it's an implicitly defined tuple then does that mean func() (n int, msg string, err error) is an implicitly defined struct!? If that's a struct, how do I access the fields if I'm not in the same package!)

I still think sum types provide many benefits, but they don't do anything to fix the issues with scoping, of course. If anything they could make it worse because you could shadow the entire 'result or error' sum instead of just shadowing the error case when you had something in the result case.

@jba I don't see how that's a desirable property. Other than a general lack of ease with the concept of making the control flow two dimensional, so to speak, I can't really think of why it's not, either, though. Can you explain the benefit to me?

Without introducing tuple types […] you'd have to [bundle] everything up in a struct if you want to use sum types to model "this or an error".

I'm ok with that: I think we'd have much more readable call sites that way (no more accidentally-transposed positional bindings!), and #12854 would mitigate a lot of the overhead currently associated with struct returns.

The big problem is migration: how would we get from the "values and error" model of Go 1 to a potential "values or error" model in Go 2, especially given APIs like io.Writer that really do return "values and error"?

I still think sum types provide many benefits, but they don't do anything to fix the issues with scoping, of course.

That depends on how you unpack them, which I suppose brings us back around to where we are today. If you prefer unions, perhaps you can envision a version of =? as an "asymmetric pattern-matching" API:

i := match strconv.Atoi(str) | err error { return err }

Where match would be the traditional ML-style pattern match operation, but the in the case of a nonexhaustive match would return the value (as an interface{} if the union has more than one unmatched alternative) rather than panicking with a nonexhaustive match failure.

I've just checked in a package at https://github.com/mpvl/errd that addresses the issues discussed here programmatically (no language changes). The most important aspect of this package is that it not only shortens error handling, but also makes it easier to do it correctly. I give examples in the docs on how traditional idiomatic error handling is more tricky than it seems, especially in the interaction with defer.

I consider this a "burner" package, though; the aim is to get some good experience and insight how best to extend the language. It interacts quite well with generics, btw, if this would become a thing.

Still working on some more examples, but this package is ready to experiment with.

@bcmills a million :+1: for #12854

As you noted, there are "return X and error" and "return X or error" so you couldn't really get around that without some macro that translates the old way into the new way on demand (and of course there would be bugs or at least runtime panics when it was inevitably used it for an "X and error" func).

I really don't like the idea of introducing special macros into the language, especially if it's just for error handling, which is my major problem with a lot of these proposals.

Go's not big on sugar or magic and that's a plus.

There's too much inertia for and too little information encoded in the current practice to handle a mass leap to a more functional error handling paradigm.

If Go 2 gets sum types—which would frankly shock me (in a good way!)—it would, if anything, have to be a very slow gradual process to move to the "new style" and in the meantime there would be even more fragmentation and confusion in how to handle errors, so I don't see it being a net positive. (I would however immediately start using it for stuff like chan union { Msg1 T; Msg2 S; Err error } instead of three channels).

If this were pre-Go1 and the Go team could say "we're just going to spend the next six months moving everything over and when it breaks stuff, keep up" that would be one thing, but right now we're basically stuck even if we do get sum types.

As you noted, there are "return X and error" and "return X or error" so you couldn't really get around that without some macro that translates the old way into the new way on demand

As I tried to say above, I don't think it's necessary for the new way, whatever it is, to cover "return X and error". If the vast majority of cases are "return X or error", and the new way improves only that, then that's great, and you can still use the old, Go 1 compatible way for the rarer "return X and error".

@nigeltao True, but we would still need some way to tell them apart during the transition, unless you're proposing that we either keep the whole standard library in the existing style.

@jimmyfrasche I don't think I can construct an argument for it. You can watch my talk or see the example in the repo README. But if the visual evidence isn't compelling for you, then there's nothing I can say.

@jba watched the talk and read the README. I now understand where you're coming from with the parenthetical/footnote/endnote/sidenote thing (and I am a fan of sidenotes (and parentheticals)).

If the goal is to put, for lack of a better term, the unhappy path to the side, then an $EDITOR plugin would work without language change and it would work with all existing code, regardless of the preferences of the code's author.

A language change makes the syntax somewhat more compact. @bcmills mentions this improves scoping but I don't really see how it could unless it has different scoping rules than := but that just seems like it would cause more confusion.

@bcmills I don't understand your comment. You can obviously tell them apart. The old way looks like this:

err := foo()
if err != nil {
  return n, err  // n can be non-zero
}

The new way looks like

check foo()

or

foo() || &FooError{err}

or whatever the bikeshed color is. I'm guessing most of the standard library can transition, but not all of it has to.

To add to @ianlancetaylor's requirements: simplifying error messages should not just make things shorter, but also easier to error handling right. Passing panics and downstream errors up to defer functions is tricky to get right.

Consider for example, writing to a Google Cloud Storage file, where we want to abort writing the file on any error:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer func() {
        if r := recover(); r != nil {
            w.CloseWithError(fmt.Errorf("panic: %v", r))
            panic(r)
        }
        if err != nil {
            _ = w.CloseWithError(err)
        } else {
            err = w.Close()
        }
    }
    _, err = io.Copy(w, r)
    return err
}

The subtleties of this code include:

  • The error from Copy is sneakily passed via the named return argument to the defer function.
  • To be fully safe, we catch panics from r and ensure we abort writing before resuming the panic.
  • Ignoring the error of the first Close is intentional, but kinda looks like a lazy-programmer artifact.

Using package errd, this code looks like:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client, err := storage.NewClient(ctx)
        e.Must(err)
        e.Defer(client.Close, errd.Discard)

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        e.Defer(w.CloseWithError)

        _, err = io.Copy(w, r)
        e.Must(err)
    })
}

errd.Discard is an error handler. Error handlers can also be used to wrap, log, whatever errors.

e.Must is the equivalent of foo() || wrapError

e.Defer is extra and deals with passing errors to defers.

Using generics, this piece of code could look something like:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client := e.Must(storage.NewClient(ctx))
        e.Defer(client.Close, errd.Discard)

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        e.Defer(w.CloseWithError)

        _ = e.Must(io.Copy(w, r))
    })
}

If we standardize on the methods to use for Defer, it could even look like:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client := e.DeferClose(e.Must(storage.NewClient(ctx)), errd.Discard)
       e.Must(io.Copy(e.DeferClose(client.Bucket(bucket).Object(dst).NewWriter(ctx)), r)
    })
}

Where DeferClose picks either Close or CloseWithError. Not saying this is better, but just showing the possibilities.

Anyway, I gave a presentation at an Amsterdam meetup last week on this topic and it seemed that the ability to make it easier to do error handling right is considered more useful than making it shorter.

A solution that improves on errors should focus at least as much on making it easier to get things right than on making things shorter.

@ALTree errd handles the "sophisticated error-recovery" out of the box.

@jimmyfrasche: errd does roughly what your playground example does, but also weaves in passing errors and panics to defers.

@jimmyfrasche: I agree that most proposals don't add much to what can already be achieved in code.

@romainmenke: agree that there is too much focus on brevity. To make it easier to do things correctly should have a bigger focus.

@jba: the errd approach makes it fairly easy to scan error versus non-error flow by just looking at the left side (anything starting with e. is error or defer handling). It also makes it very easy to scan which of the return values are handled for error or defer and which not.

@bcmills: although errd doesn't fix the scoping issues in and of itself, it eliminates the need to pass downstream errors to earlier declared error variables and all, thereby mitigating the problem considerably for error handling, AFAICT.

errd seems to rely entirely on panics and recovery. that sounds like it comes with a significant performance penalty. I'm not sure it is an overall solution because of this.

@urandom: Under the hood it is implemented as a more costly, but single defer.
If the original code:

  • uses no defer: the penalty of using errd is large, about 100ns*.
  • uses idiomatic defer: the running time or errd is of the same order, although somewhat slower
  • uses proper error handling for defer: the running time is about equal; errd can be faster if the number of defers is > 1

Other overheads:

  • Passing closures (w.Close) to Defer currently also adds about a 25ns* overhead, compared to using DeferClose or DeferFunc API (see release v0.1.0). After discussing with @rsc I removed it to keep the API simple and worry about optimizing later.
  • Wrapping inline error strings as handlers (e.Must(err, msg("oh noes!")) costs about 30ns with Go 1.8. With tip (1.9), however, though still an allocation, I clocked the cost at 2ns. Of course for pre-declared error messages the cost is still negligible.

(*) all numbers running on my 2016 MacBook Pro.

All in all, the cost seems acceptable if your original code uses defer. If not, Austin is working on significantly reducing the cost of defer, so the cost may even go down over time.

Anyway, the point of this package is to get experience on how using alternative error handling would feel and be useful now so we can build the best language addition in Go 2. Case in point is the current discussion, it focusses too much on reducing a few lines for simple cases, whereas there is much more to gain and arguably other points are more important.

@jimmyfrasche:

then an $EDITOR plugin would work without language change

Yes, that is just what I argue in the talk. Here I'm arguing that if we make a language change, it should be in accord with the "sidenote" concept.

@nigeltao

You can obviously tell them apart. The old way looks like this:

I'm talking about the point of declaration, not the point of use.

Some of the proposals discussed here do not distinguish between the two at the call site, but some do. If we choose one of the options that assumes "value or error" — such as ||, try … catch, or match — then it should be a compile-time error to use that syntax with a "value and error" function, and it should be up to the implementor of the function to define which it is.

At the point of declaration, there is currently no way to distinguish between "value and error" and "value or error":

func Atoi(string) (int, error)

and

func WriteString(Writer, String) (int, error)

have the same return types, but different error semantics.

@mpvl I'm looking at the docs and src for errd. I think I'm starting to understand how it works but it looks like it has a lot of API that gets in the way of understanding that seems as if it could be implemented in separate package. I'm sure that all makes it more useful in practice but as an illustration it adds a lot of noise.

If we ignore common helpers like top-level funcs for operating on the result of WithDefault(), and assume, for the sake of simplicity that we always used context, and ignore any decisions made for performance would the absolute minimal barebone API reduce to the below operations?

type Handler = func(ctx context.Context, panicing bool, err error) error
Run(context.Context, func(*E), defaults ...Handler) //egregious style but most minimal
type struct E {...}
func (*E) Must(err error, handlers ...Handler)
func (*E) Defer(func() error, handlers ...Handler)

Looking at the code I see some good reasons it's not defined as above, but I'm trying to get at the core semantics in order to better understand the concept. For example, I'm not sure if IsSentinel is in the core or not.

@jimmyfrasche

@bcmills mentions this improves scoping but I don't really see how it could

The main improvement is in keeping the err variable out of scope. That would avoid bugs such as the ones linked from https://github.com/golang/go/issues/19727. To illustrate with a snippet from one of them:

    res, err := ctxhttp.Get(ctx, c.HTTPClient, dirURL)
    if err != nil {
        return Directory{}, err
    }
    defer res.Body.Close()
    c.addNonce(res.Header)
    if res.StatusCode != http.StatusOK {
        return Directory{}, responseError(res)
    }

    var v struct {
        …
    }
    if json.NewDecoder(res.Body).Decode(&v); err != nil {
        return Directory{}, err
    }

The bug occurs in the last if-statement: the error from Decode is dropped, but it's not obvious because an err from an earlier check was still in scope. In contrast, using the :: or =? operator, that would be written:

    res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) =? err { return Directory{}, err }
    defer res.Body.Close()
    c.addNonce(res.Header)
    (res.StatusCode == http.StatusOK) =! { return Directory{}, responseError(res) }

    var v struct {
        …
    }
    json.NewDecoder(res.Body).Decode(&v) =? err { return Directory{}, err }

Here there are two scoping improvements that help:

  1. The first err (from the earlier Get call) is only in scope for the return block, so it cannot be used accidentally in subsequent checks.
  2. Because the err from Decode is declared in the same statement in which it is checked for nilness, there can be no skew between the declaration and the check.

(1) alone would have been sufficient to reveal the error at compile time, but (2) makes it easy to avoid when using the guard statement in the obvious way.

@bcmills thanks for the clarification

So, in res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) =? err { return Directory{}, err } the =? macro expands to

var res *http.Reponse
{
  var err error
  res, err = ctxhttp.Get(ctx, c.HTTPClient, dirURL)
  if err != nil {
    return Directory{}, err 
  }
}

If that's correct it's what I meant when I said it would have to have different semantics from :=.

It seems like it would cause its own confusions such as:

func f() error {
  var err error
  g() =? err {
    if err != io.EOF {
      return err
    }
  }
  //one could expect that err could be io.EOF here but it will never be so
}

Unless I've misunderstood something.

Yep, that's the correct expansion. You're right that it differs from :=, and that's intentional.

It seems like it would cause its own confusions

That's true. It's not obvious to me whether that would be confusing in practice. If it is, we could provide ":" variants of the guard statement for declaration (and have the "=" variants only assign).

(And now that's making me think the operators should be spelled ? and ! instead of =? and =!.)

res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) ?: err { return Directory{}, err }

but

func f() error {
  var err error
  g() ?= err { (err == io.EOF) ! { return err } }
  // err may be io.EOF here.
}

@mpvl My main concern about errd is with the Handler interface: it seems to encourage functional-style pipelines of callbacks, but my experience with callback / continuation style code (both in imperative languages like Go and C++ and in functional languages like ML and Haskell) is that it is often much harder to follow than the equivalent sequential / imperative style, which also happens to align with the rest of Go idioms.

Do you envision Handler-style chains as part of the API, or is your Handler a stand-in for some other syntax you are considering (such as something operating on Blocks?)

@bcmills I'm still not on board with the magic features that introduce dozens of concepts into the language into a single line and only work with one thing, but I finally get why they're more than just a slightly shorter way to write x, err := f(); if err != nil { return err }. Thanks for helping me understand and sorry it took so long.

@bcmills I rewrote @mpvl's motivating example, which has some gnarly error handling, using the latest =? proposal that does not always declare a new err variable:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)

        defer func() {
                if r := recover(); r != nil { // r is interface{} not error so we can't use it here
                        _ = w.CloseWithError(fmt.Errorf("panic: %v", r))
                        panic(r)
                }

                if err != nil { // could use =! here but I don't see how that simplifies anything
                        _ = w.CloseWithError(err)
                } else {
                        err = w.Close()
                }
        }()

        io.Copy(w, r) =? err { return err } // what about n? does this need to be prefixed by a '_ ='?
        return nil
}

The majority of the error handling is unchanged. I could only use =? in two places. In the first place it didn't really provide any benefit that I could see. In the second it made the code longer and obscures the fact that io.Copy returns two things, so it probably would have been better to just not use it there.

@jimmyfrasche That code is the exception, not the rule. We shouldn't design features to make it easier to write.

Also, I question whether the recover should even be there. If w.Write or r.Read (or io.Copy!) is panicking, it's probably best to terminate.

Without the recover, there's no real need for the defer, and the bottom of the function could become

_ = io.Copy(w, r) =? err { _ = w.CloseWithError(err); return err }
return w.Close()

@jimmyfrasche

// r is interface{} not error so we can't use it here

Note that my specific wording (in https://github.com/golang/go/issues/21161#issuecomment-319434101) is about zero-values, not errors specifically.

// what about n? does this need to be prefixed by a '_ ='?

It does not, although I could have been more explicit about that.

I don't particularly like @mpvl's use of recover in that example: it encourages the use of panic over idiomatic control flow, whereas if anything I think we should eliminate extraneous recover calls (such as the ones in fmt) from the standard library in Go 2.

With that approach, I would write that code as:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        io.Copy(w, r) =? err {
                w.CloseWithError(err)
                return err
        }
        return w.Close()
}

On the other hand, you are correct that with the unidiomatic recover there is little opportunity to apply features intended to support idiomatic error handling. However, separating the recovery from the Close operation does lead to IMO somewhat cleaner code.

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        defer func() {
                if err != nil {
                        _ = w.CloseWithError(err)
                } else {
                        err = w.Close()
                }
        }()
        defer func() {
                recover() =? r {
                        err = fmt.Errorf("panic: %v", r)
                        panic(r)
                }
        }()

        io.Copy(w, r) =? err { return err }
        return nil
}

@jba the defer handler re-panics: it's there to attempt to notify the process on the other computer so it doesn't accidentally commit a bad transaction (assuming that's still possible in a potential error state). If that's not fairly common it probably should be. I agree that Read/Write/Copy shouldn't panic but if there was other code in there that could reasonably panic, for whatever reason, we'd be right back where we started.

@bcmills that last revision does look better (even if you took out the =?, really)

@jba:

_ = io.Copy(w, r) =? err { _ = w.CloseWithError(err); return err }
return w.Close()

that still doesn't cover the case of a panic in the reader. Admittedly this is a rare case, but quite an important one: calling Close here in case of a panic is very bad.

That code is the exception, not the rule. We shouldn't design features to make it easier to write.

@jba: I wholeheartedly disagree in this case. It is important to get error handling right. Allowing the simple case to be easier will encourage people even less to think about proper error handling. I would like to see some approach that, like errd, makes conservative error handling trivially easy, while requiring some effort to relax rules, not anything that moves even slightly in the other direction.

@jimmyfrasche: regarding your simplification: you're roughly right.

  • IsSentinel is not essential, just handy and common. I dropped it, for now at least.
  • The Err in State is different from err, so your API drops this. It is not critical for understanding, though.
  • Handlers could be functions, but are interfaces mostly for performance reasons. I just know that a good bunch of people won't use the package if it is not optimized. (see some of the first comments on errd in this issue)
  • The context is unfortunate. AppEngine needs it, but not much else I reckon. I would be fine removing support for it until people balk.

@mpvl I was just trying to whittle it down to a few things so it was easier to wrap my head around how it worked, how to use it, and to imagine how it would fit in with code I wrote.

@jimmyfrasche: understood, although it is good if an API doesn't require you to do that. :)

@bcmills: Handlers serve a few purposes, for instance, in order of importance:

  • wrap an error
  • define to ignore errors (to make that explicit. See example)
  • log errors
  • error metrics

Again in order of importance, these need to be scoped by:

  • block
  • line
  • package

The default errors are just there to make it easier to guarantee that an error is handled somewhere,
but I could live with block-level only. I originally had an API with Options instead of Handlers. That resulted in a larger and more clumsy API in addition to being slower, though.

I don't see the callback issue being that bad here. Users define a Runner by passing it a Handler that gets called if there is an error. The specific runner is explicitly specified in the block where the errors are handled. In many cases, a handler will just be some wrapped string literal passed inline. I will take some playing around to see what is useful and what is not.

BTW, If we should not encourage logging errors in Handlers, then the Context support can probably be dropped.

@jba:

Also, I question whether the recover should even be there. If w.Write or r.Read (or io.Copy!) is panicking, it's probably best to terminate.

writeToGS still terminates if there is a panic, as it should(!!!), it merely ensures it calls CloseWithError with a non-nil error. If the panic is not handled, the defer is still called, but with err == nil, resulting in a potentially corrupt file materializing on Cloud Storage. The right thing to do here is to call CloseWithError with some temporary error and then continue the panic.

I found a bunch of examples like this in Go code. Dealing with io.Pipes also often result in a little bit too subtle code. Handling errors is often not as straightforward as it seems as you saw now yourself.

@bcmills

I don't particularly like @mpvl's use of recover in that example: it encourages the use of panic over idiomatic control flow,

Not trying to encourage the use of panic any bit. Note that the panic is re-raised right after CloseWithError and thus does not otherwise change control flow. A panic stays a panic.
Not using recover here is wrong, though, as a panic will cause the defer to be called with a nil error, signaling that what has been written so far can be committed.

The only somewhat valid argument not to use recover here is that it is very unlikely for a panic to occur, even for an arbitrary Reader (the Reader is of unknown type in this example for a reason :) ).
However, for production code this is an unacceptable stance. Especially when programming at a large enough scale this is bound to happen sometime (panics can be caused by other things than bugs in code).

BTW, note that the errd package eliminates the need for the user to think about this. Any other mechanism that signals an error in case of a panic to defers is fine, though. Not calling defers on panic would work too, but that comes with its own issues.

At the point of declaration, there is currently no way to distinguish between "value and error" and "value or error":

@bcmills Oh, I see. To open another can of bikesheds, I suppose that you could say

func Atoi(string) ?int

instead of

func Atoi(string) (int, error)

but WriteString would remain unchanged:

func WriteString(Writer, String) (int, error)

I like the =?/=!/:=?/:=! proposal by @bcmills / @jba better than similar proposals. It has some nice properties:

  • composable (you can use =? within a =? block)
  • general (only cares about the zero value, not specific to the error type)
  • improved scoping
  • could work with defer (in one variation above)

It also has some properties I do not find as nice.

Composition nests. Repeated use is going to continue to indent farther and farther right. That's not necessarily a bad thing in itself, but I would imagine that in situations with very complicated error handling that requires dealing with errors causing errors causing errors that the code to deal with it would quickly get much less clear than the current status quo. In such a situation one could use =? for the outermost error and if err != nil in the inner errors but then has this really improved error handling in general or just in the common case? Maybe improving the common case is all that's needed, but I don't find it compelling, personally.

It introduces falsiness into the language to gain its generality. Falsiness being defined as "is (not) the zero value" is perfectly reasonable, but if err != nil { is better than if err { since it is explicit, in my opinion. I would expect to see contortions in the wild that try to use =?/etc. over more natural control flow to try to get access to its falsiness. That would certainly be unidiomatic and frowned upon, but it would happen. While potential abuse of a feature is not in itself an argument against a feature, it is something to consider.

The improved scoping (for the variants that declare their parameter) is nice in some cases, but if scoping needs to be fixed, fix scoping in general.

The "only rightmost result" semantics make sense but seems a little strange to me. That's more of a feeling than an argument.

This proposal adds brevity to the language but no additional power. It could be implemented entirely as a preprocessor that does macro expansion. That would of course be undesirable: it would complicate builds and fragment development and any such preprocessor would be extremely complicated since it has to be type-aware and hygienic. I'm not trying to dimiss by saying "just make a preprocessor". I bring this up solely to point out that this proposal is entirely sugar. It does not let you do anything you could not do in Go now; it just lets you write it more compactly. I'm not dogmatically opposed to sugar. There is power in a carefully chosen linguistic abstraction, but the fact that it is sugar means that it should be considered 👎 until proven innocent, so to speak.

The lhs of the operators are a statement but a very limited subset of statements. Which elements to include in that subset is fairly self evident, but, if nothing else, it would require refactoring the grammar in the language spec to accommodate the change.

Would something like

func F() (S, T, error)

func MustF() (S, T) {
  return F() =? err { panic(err) }
}

be allowed?

If

defer f.Close() :=? err {
    return err
}

is allowed that must be (somehow) equivalent to

func theOuterFunc() (err error) {
  //...
  defer func() {
    if err2 := f.Close(); err2 != nil {
      err = err2
    }
  }()
  //...
}

which seems deeply problematic and likely to cause very confusing situations, even ignoring that in a very un-Go-like manner it hides the performance implications of implicitly allocating a closure. The alternative is to have return return from the implicit closure and then have an error message that you can't return a value of type error from a func() which is a bit obtuse.

Really though, aside from a slightly improved scoping fix, this doesn't fix any of the problems I face dealing with errors in Go. At most typing if err != nil { return err } is a nuisance, modulo the slight readability concerns I expressed in #21182. The two biggest problems are

  1. thinking about how to handle the error—and there's nothing a language can do about that
  2. introspecting an error to determine what to do in some situations—some additional convention with support from the errors package would go a long way here, though they cannot solve all the problems.

I realize that those aren't the only problems and that many find other aspects more immediately concerning, but they are the ones I spend the most time with and find more vexing and troublesome than anything else.

Better static analysis to detect when I've messed something up would always be appreciated, of course (and in general, not just this scenario). Language changes and conventions making it easier to analyze the source so these are more useful would also be of interest.

I just wrote a lot (A LOT! sorry!) about this but I'm not dismissing the proposal. I do think it has merit, but I am not convinced that it clears the bar or pulls its weight.

@jimmyfrasche

I remember when the current behavior of := was introduced—a lot of that thread on go nuts† was clamor for a way to explicitly annotate which names to be reused instead of the implicit "reuse only if that variable exists in exactly the current scope" which is where all the subtle hard-to-see problems manifest, in my experience.

† I cannot find that thread does anyone have a link?

I think you must be remembering a different thread, unless you were involved in Go when it was released. The spec from 2009/11/9, just before it was released, has:

Unlike regular variable declarations, a short variable declaration may redeclare variables provided they were originally declared in the same block with the same type, and at least one of the non-blank variables is new.

I remember seeing that when reading the spec for the first time and thinking that was a great rule, as I'd previously used a language with := but without that reuse rule, and thinking of new names for the same thing was tedious.

@mpvl
I think that the trickiness of your original example is more a result of the
the API you're using there than of Go's error handling itself.

It is an interesting example though, particularly because of the fact that
you don't want to close the file normally if you get a panic, so the
normal "defer w.Close()" idiom doesn't work.

If you didn't need to avoid calling Close when there's a
panic, then you could do:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer w.Close()
    _, err = io.Copy(w, r)
    if err != nil {
        w.CloseWithError(err)
    }
    return err
}

assuming that the semantics were changed such that calling Close
after calling CloseWithError is a no-op.

I don't think that looks so bad any more.

Even with the requirement that the file doesn't get written with no error when there's a panic shouldn't be too hard to accommodate; for example by adding a Finalize function that must be called explicitly before Close.

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer w.Close()
    _, err = io.Copy(w, r)
    return w.Finalize(err)

That can't attach the panic error message though, but decent logging could make that clearer.
(the Close method could even have a recover call inside it, although I'm not sure if that's
actually a really bad idea...)

However, I think the panic-recovery aspect of this example is somewhat of a red herring in this context, as 99+% cases of error handling don't do panic recovery.

@rogpeppe:

That can't attach the panic error message though, but decent logging could make that clearer.

I don't think that is an issue.

Your proposed API change mitigates but doesn't fully solve the problem yet, though. The required semantics requires other code to behave properly too. Consider the below example:

r, w := io.Pipe()
go func() {
    var err error                // used to intercept downstream errors
    defer func() {
        w.CloseWithError(err)
    }()

    r, err := newReader()
    if err != nil {
        return
    }
    defer func() {
        if errC := r.Close(); errC != nil && err == nil {
            err = errC
        }
    }
    _, err = io.Copy(w, r)
}()
return r

In its own right, this code shows that error handling can be tricky or is at least messy (and I would be curious how this could be improved with the other proposals): it stealthily passes downstream errors up through a variable and has a bit too clunky if statement to ensure that the right error is passed. Both distract too much from the "business logic". Error handling dominates the code. And this example doesn't even handle panics yet.

For completeness, in errd this _would_ handle panics correctly and would look like:

r, w := io.Pipe()
go errd.Run(func(e *errd.E) {
    e.Defer(w.CloseWithError)

    r, err := newReader()
    e.Must(err)
    e.Defer(r.Close)

    _, err = io.Copy(w, r)
    e.Must(err)
})
return r

If the above reader (not using errd) is passed as the reader to writeToGS, and the io.Reader returned by newReader panics, it would still result in faulty semantics with your proposed API fix (might race into successfully closing the GS file after the pipe is closed on panic with a nil error.)

This also proves the point. It is non-trivial to reason about proper error handling in Go. When I looked at how code would look like rewriting it with errd, I found a bunch of buggy code. I really only learned how difficult and subtle it was to write proper idiomatic Go error handling, though, when writing the unit tests for the errd package. :)

An alternative to your proposed API change would be to not handle any defers at all on panic. This has its own issues and wouldn't fully solve the problem and is probably undoable, but would have some nice qualities.

Either way, best would be some language change that mitigates the subtleties of error handling, rather than one that focuses on brevity.

@mpvl
I often find with error handling code in Go that creating another function can clean things up. I would write your code above something like this:

func something() {
    r, w := io.Pipe()
    go func() {
        err := copyFromNewReader(w)
        w.CloseWithError(err)
    }()
    ...
}

func copyFromNewReader(w io.Writer) error {
    r, err := newReader()
    if err != nil {
        return err
    }
    defer r.Close()
    _, err = io.Copy(w, r)
    return err
}()

I am assuming that r.Close doesn't return a useful error - if you've read all the way through a reader and just encountered only io.EOF, then it almost certainly doesn't matter if it returns an error when closed.

I'm not keen on the errd API - it's too sensitive to goroutines being started. For example: https://play.golang.org/p/iT441gO5us Whether doSomething does or does not start a goroutine to run the
function in should not affect the correctness of the program, but when using errd, it does. You're expecting panics to safely traverse abstraction boundaries, and they don't in Go.

@mpvl

defer w.CloseWithError(err)

BTW, this line always calls CloseWithError with a nil error value. I think you meant to
write:

defer func() { 
   w.CloseWithError(err)
}()

@mpvl

Note that the error returned by the Close method on an io.Reader is almost never useful (see the list in https://github.com/golang/go/issues/20803#issuecomment-312318808).

That suggests that we should write your example today as:

r, w := io.Pipe()
go func() (err error) {
    defer func() { w.CloseWithError(err) }()

    r, err := newReader()
    if err != nil {
        return err
    }
    defer r.Close()

    _, err = io.Copy(w, r)
    return err
}()
return r

...which seems perfectly fine to me, apart from being a bit verbose.

It's true that it passes a nil error to w.CloseWithError in case of a panic, but the whole program terminates at that point anyway. If it's important never to close with a nil error, it's a simple rename plus one extra line:

-go func() (err error) {
-   defer func() { w.CloseWithError(err) }()
+go func() (rerr error) {
+   rerr = errors.New("goroutine exited by panic")
+   defer func() { w.CloseWithError(rerr) }()

@rogpeppe: indeed, thanks. :)

Yes, I'm aware of the goroutine issue. It is nasty, but probably something that is not hard to catch with a vet check. Anyway, I don't see errd as a final solution but more as a way to get experience on how to best address error handling. Ideally there would be a language change that solves the same problems, but with the proper restrictions imposed.

You're expecting panics to safely traverse abstraction boundaries, and they don't in Go.

That is not what I'm expecting. In this case, I'm expecting APIs to not report success when there was not. Your last piece of code handles it correctly, because it doesn't use defer for the writer. But this is very subtle. Many users would use defer in this case because it is considered idiomatic.

Maybe a set of vet checks could catch problematic uses of defers. Still, in both the original "idiomatic" code and your last refactored piece there is a lot of tinkering to work around subtleties of error handling for something that is otherwise a pretty simple piece of code. The workaround code is not for figuring out how to handle certain error cases, it is purely a waste of brain cycles that could be used for productive use.

Specifically what I'm trying to learn from errd is whether it makes error handling more straightforward when used straightforwardly. From what I can see many complications and subtleties vanish. It would be good to see if we can codify aspects of its semantics into new language features.

@jimmyfrasche

It introduces falsiness into the language to gain its generality.

That's a very good point. The usual problems with falsiness come from forgetting to invoke a boolean function or forgetting to dereference a pointer to nil.

We could address the latter by defining the operator to only work with nillable types (and probably dropping =! as a result, since it would be mostly useless).

We could address the former by further restricting it to not work with function types, or to only work with pointer or interface types: then it would be clear that the variable is not a boolean, and attempts to use it for boolean comparisons would be more obviously wrong.

Would something like [MustF] be allowed?

Yes.

If [defer f.Close() :=? err {] is allowed that must be (somehow) equivalent to
[defer func() { … }()].

Not necessarily, no. It could have its own semantics (more like call/cc than an anonymous function). I haven't proposed a spec change for using =? in defer (it would require at least a change in the grammar), so I'm not sure exactly how complicated such a definition would be.

The two biggest problems are […] 2. introspecting an error to determine what to do in some situations

I agree that that's a bigger problem in practice, but it seems more-or-less orthogonal to this issue (which is more about reducing boilerplate and the associated potential for errors).

(@rogpeppe, @davecheney, @dsnet, @crawshaw, I, and a few others I'm surely forgetting had a nice discussion at GopherCon about APIs for inspecting errors, and I hope we'll see some good proposals on that front too, but I really think that's a matter for another issue.)

@bcmills: this code has two problems 1) same as @rogpeppe mentioned: err passed to CloseWithError is always nil, and 2) it still doesn't handle panics so that means the API will report success explicitly when there is a panic (the returned r might emit an io.EOF even when not all bytes have been written), even if 1 is fixed.

Otherwise I'm agree that the error returned by Close can often be ignored. Not always, though (see first example).

I do find it somewhat surprising that there were like 4 or 5 faulty suggestions were made on my rather straightforward examples (including one from myself) and I still feel like I have to argue that error handling in Go is not trivial. :)

@bcmills:

It's true that it passes a nil error to w.CloseWithError in case of a panic, but the whole program terminates at that point anyway.

Does it? Defers of that goroutine still get called. As far as I understand they will run to completion. In this case the Close will signal an io.EOF.

See, for example, https://play.golang.org/p/5CFbsAe8zF. After the goroutine panics it still happily passes "foo" to the other goroutine which then still makes it to write it to Stdout.

Similarly, other code may receive an incorrect io.EOF from a panicking goroutine (like the one in your example), conclude success, and happily commit a file to GS before the panicking goroutine resumes its panic.

You're next argument may be: well don't write buggy code, but:

  • then make it easier to prevent these bugs, and
  • panics may be caused by external factors, such as OOMs.

If it's important never to close with a nil error, it's a simple rename plus one extra line:

It should still close with nil to signal io.EOF when it is finished, so that won't work.

If it's important never to close with a nil error, it's a simple rename plus one extra line:

It should still close with nil to signal io.EOF when it is finished, so that won't work.

Why not? The return err at the end will set rerr to nil.

@bcmills: ah I see what you mean now. Yes, that should work. I'm not worried about the number of lines, though, but rather about the subtlety of the code.

I find this to be in the same category of problems as the variable shadowing, just less likely to run in to (possibly making it worse.) Most variable shadowing bugs you'll arguable run in to with good unit tests. Arbitrary panics are harder to test for.

When operating at scale it is pretty much guaranteed you'll see bugs like this manifesting. I may be paranoid, but I've seen far less likely scenarios lead to data loss and corruption. Normally this is fine, but not for transaction processing (like writing gs files.)

I hope you don't mind me hijacking your proposal with an alternate syntax - how do folks feel about something like this:

return err if f, err := os.Open("..."); err != nil

@SirCmpwn That buries the lede. The easiest thing to read in a function should be the normal flow of control, not the error handling.

That's fair, but your proposal makes me uncomfortable as well - it introduces an opaque syntax (||) that behaves differently from how users have been trained to expect || to behave. Not sure what the right solution is, will mull over it some more.

@SirCmpwn Yes, as I said in the original post "I'm writing this proposal mainly to encourage people who want to simplify Go error handling to think about ways to make it easy to wrap context around errors, not just to return the error unmodified." I wrote my proposal as well as I could, but I do not expect it to be adopted.

Understood.

This is a bit more radical, but maybe a macro-driven approach would work better.

f = try!(os.Open("..."))

try! would eat the last value in the tuple and return it if not nil, and otherwise return the rest of the tuple.

I'd like to suggest that our problem statement is,

Error handling in Go is verbose and repetitive. The idiomatic format of Go's error handling makes it more difficult to see the non-error control flow and the verbosity is unappealing, especially to newcomers. To date, solutions proposed for this problem typically require artisanal one-off error handling functions, reduce locality of error handling, and increase complexity. Because one of Go's goals is to force the writer to consider error handling and recovery, any improvement to error handling should also build upon that goal.

To address this problem statement, I propose these goals for error handling improvements in Go 2.x:

  1. Reduces repetitive error handling boilerplate and maximizes focus on the primary intent of the code path.
  2. Encourages proper error handling, including wrapping of errors when propagating them onward.
  3. Adheres to the Go design principles of clarity and simplicity.
  4. Is applicable in the broadest possible range of error handling situations.

Evaluating this proposal:

f.Close() =? err { return fmt.Errorf(…, err) }

according to those goals, I would conclude that it succeeds well on goal #1. I'm not sure how it helps with #2, but it doesn't make adding context any less likely either (my own proposal shared this weakness on #2). It doesn't really succeed at #3 and #4, though:
1) As others have said, the error value checking and assignment is opaque and unusual; and
2) The =? syntax is also unusual. It's especially confusing if combined with the similar but different =! syntax. It will take a while for people to get used to their meanings; and
3) Returning a valid value along with the error is common enough that any new solution should also handle that case.

Making the error handling a block might be a good idea, though, if as others have suggested, it's combined with changes to gofmt. Relative to my proposal, it improves generality, which should help with goal #4 and familiarity which helps goal #3 at the cost of a sacrifice in brevity for the common case of simply returning the error with added context.

If you had asked me in the abstract, I might have agreed that a more general solution would be preferable to an error handling specific solution as long as it met the error handling improvement goals above. Now though, having read this discussion and thought about it more, I'm inclined to believe an error handling specific solution will result in greater clarity and simplicity. While errors in Go are just values, error handling constitutes such a significant portion of any programming that having some specific syntax to the make error handling code clear and concise seems appropriate. I'm afraid we will make an already hard problem (coming up with a clean solution for error handling) even harder and more complicated if we conflate it with other goals such as scoping and composability.

Regardless,though, as @rsc points out in his article, Toward Go 2, neither the problem statement, the goals nor any syntax proposal is likely to advance without experience reports that demonstrate that the problem is significant. Maybe instead of debating various syntax proposals, we should start digging for supporting data?

Regardless,though, as @rsc points out in his article, Toward Go 2, neither the problem statement, the goals nor any syntax proposal is likely to advance without experience reports that demonstrate that the problem is significant. Maybe instead of debating various syntax proposals, we should start digging for supporting data?

I think that this is self evident if we assume that ergonomics is important. Open any Go codebase and look for places where there are opportunities to DRY things up and/or improve ergonomics that the language can address - right now error handling is a clear outlier. I think the Toward Go 2 approach may erroneously advocate for disregarding problems that have workarounds - in this case that people just grin and bear it.

if $val, err := $operation($args); err != nil {
  return err
}

When there's more boilerplate than code, the problem is self-evident imho.

@billyh

I feel that the format: f.Close() =? err { return fmt.Errorf(…, err) } is overly verbose and confusing. I personally don't feel that the error part should be in a block. Inevitably, that would lead it to be spread out in 3 lines instead of 1. Furthermore, in the off change that you need to do more than just modify an error prior to returning it, one can just use the current if err != nil { ... } syntax.

The =? operator is also a tad confusing. It's not immediately obvious what's happening there.

With something like this:
file := os.Open("/some/file") or raise(err) errors.Wrap(err, "extra context")
or the shorthand:
file := os.Open("/some/file") or raise
and the deferred:
defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2)
is a bit more wordy and the choice of word could reduce the initial confusion (i.e. people might immediately associate raise with a similar keyword from other languages like python, or just deduce that raise raises the error/last-non-default-value up the stack to the caller).

It's also a good imperative solution, which doesn't try to solve every possible obscure error handling under the sun. By far, the largest chunk of error handling in the wild is of the above-mentioned nature. For the later, the current syntax is also there to help.

Edit:
If we want to reduce the "magic" a bit, the previous examples might also look like:
file, err := os.Open("/some/file") or raise errors.Wrap(err, "extra context")
file, err := os.Open("/some/file") or raise err
defer err2 := f.Close() or errors.ReplaceIfNil(err, err2)
I personally think the previous examples are better, since they move the full error handling to the right, instead of splitting it as is the case here. This might be clearer though.

I'd like to suggest that our problem statement is, ...

I don't agree with the problem statement. I'd like to suggest an alternative:


Error handling does not exist from the language point of view. The only thing Go provides is a predeclared error type and even that is just for convenience because it does not enable anything really new. Errors are just values. Error handling is just normal user code. There is nothing special about it from the language POV and there should not be anything special about it. The only problem with error handling is that some people believe this valuable and beautiful simplicity must be eliminated at any cost.

Along the lines of what Cznic says, it would be nice to have a solution that is useful for more than just error handling.

One way to make error handling more general is to think of it in terms of union-types/sum-types and unwrapping. Swift and Rust both have solutions with ? ! syntax, although I think Rust's has been a bit unstable.

If we don't want to make sum-types a high level concept, we could make it just part of multiple return, the way that tuples aren't really part of Go, but you can still do multiple return.

A stab at syntax inspired by Swift:

func Failable() (*Thingie | error) {
    ...
}

guard thingie, err := Failable() else { 
    return wrap(err, "Could not make thingie)
}
// err is not in scope here

You could use this for other things too, like:

guard val := myMap[key] else { val = "default" }

The solution =? proposed by @bcmills and @jba isn't only for error, the concept is for non zero. this example will work normally.

func Foo()(Bar, Recover){}
bar := Foo() =? recover { log.Println("[Info] Recovered:", recover)}

The main idea of this proposal is the side notes, separate the primary purpose of the code and put secundary cases aside, in order to make reading easier.
For me the reading of a Go code, in some cases, is not continuous, many times you have the stop the idea with if err!= nil {return err}, so the idea of side notes seems interesting to me, as in a book we read the main idea continually and then read the side notes. (@jba talk)
In very rare situations the error is the primary purpose of a function, maybe in a recovery. Normally when we have an error, we add some context, log and return, in these cases, side notes can make your code more readable.
I don't know if it's the best syntax, particularly I do not like the block in the second part, a side note needs to be small, a line should be enough

bar := Foo() =? recover: log.Println("[Info] Recovered:", recover)

@billyh

  1. As others have said, the error value checking and assignment is opaque and unusual; and

Please be more concrete: "opaque and unusual" are awfully subjective. Can you give some examples of code where you think the proposal would be confusing?

  1. The =? syntax is also unusual. […]

IMO that's a feature. If someone sees an unusual operator, I suspect they're more inclined to look up what it does instead of just assuming something that may or may not be accurate.

  1. Returning a valid value along with the error is common enough that any new solution should also handle that case.

It does?

Read the proposal carefully: =? performs assignments before evaluating the Block, so it could be used for that case too:

n := r.Read(buf) =? err {
  if err == io.EOF {
    […]
  }
  return err
}

And as @nigeltao noted, you can always use the existing 'n, err := r.Read(buf)` pattern. Adding a feature to help with scoping and boilerplate for the common case does not imply that we must use it for uncommon cases too.

Maybe instead of debating various syntax proposals, we should start digging for supporting data?

See the numerous issues (and their examples) that Ian linked in the original post.
See also https://github.com/golang/go/wiki/ExperienceReports#error-handling.

If you've had specific insight from those reports, please do share it.

@urandom

I personally don't feel that the error part should be in a block. Inevitably, that would lead it to be spread out in 3 lines instead of 1.

The purpose of the block is twofold:

  1. to provide a clear visual and grammatical break between the error-producing expression and its handler, and
  2. to allow for a wider range of error handling (per @ianlancetaylor's stated goal in the original post).

3 lines vs. 1 is not even a language change: if number of lines is your biggest concern, we could address that with a simple change to gofmt.

file, err := os.Open("/some/file") or raise errors.Wrap(err, "extra context")
file, err := os.Open("/some/file") or raise err

We already have return and panic; adding raise on top of those seems like it adds too many ways to exit a function for too little gain.

defer err2 := f.Close() or errors.ReplaceIfNil(err, err2)

errors.ReplaceIfNil(err, err2) would require some very unusual pass-by-reference semantics.
You could pass err by pointer instead, I suppose:

defer err2 := f.Close() or errors.ReplaceIfNil(&err, err2)

but it still seems very odd to me. Does the or token construct an expression, a statement, or something else? (A more concrete proposal would help.)

@carlmjohnson

What would the concrete syntax and semantics of your guard … else statement be? To me, it seems a lot like =? or :: with the tokens and variable positions swapped around. (Again, a more concrete proposal would help: what are the actual syntax and semantics you have in mind?)

@bcmills
The hypothetical ReplaceIfNil would be a simple:

func ReplaceIfNil(original, replacement error) error {
   if original == nil {
       return replacement
   }
   return original
}

Nothing unusual about that. Maybe the name ...

or would be a binary operator, where the left operand would be either an IdentifierList, or a PrimaryExpr. In the case of the former, it is reduced to the rightmost identifier. It then allows the right-hand operand to be executed if the left one is not a default value.

Which is why i needed another token afterwards, to do the magic of returning the default values, for all except the last parameter in the function Result, which would take the value of the expression afterwards.
IIRC, there was another proposal not too long ago which would have the language add a '...' or something, that would take the place of the tedious default value initialization. In that cause, the whole thing might look like this:

f, err := os.Open("/some/file") or return ..., errors.Wrap(err, "more context")

As for the block, I understand that it allows for wider handling. I'm personally not sure whether this proposal's scope should be to try and cater to every possible scenario, as opposed to cover a hypothetical 80%. And I personally believe that it matters how many lines a result would take (though I never said it was my biggest concern, that is in fact the readability, or lack thereof, when using obscure tokens like =?). If this new proposal spans multiple lines in the general case, I personally don't see its benefits over something like:

if f, err := os.Open("/some/file"); err != nil {
     return errors.Wrap(err, "more context")
}
  • if the above defined variables were to be made available outside the if scope.
    And that would still make a function with just a couple of such statements harder to read, due to the visual noise of these error handling blocks. And that is one of the complaints that people have when discussing error handling in go.

@urandom

or would be a binary operator, where the left operand would be either an IdentifierList, or a PrimaryExpr. […] It then allows the right-hand operand to be executed if the left one is not a default value.

Go's binary operators are expressions, not statements, so making or a binary operator would raise a lot of questions. (What are the semantics of or as part of a larger expression, and how does that jibe with the examples you've posted with :=?)

Assuming that it is actually a statement, what is the right-hand operand? If it is an expression, what is its type, and can raise be used as an expression in other contexts? If it is a statement, what are its semantics if anything other than a raise? Or are you proposing for or raise to be essentially a single statement (e.g. or raise as a syntax alternative to :: or =?)?

Can I write

defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2) or raise(err3) Transform(err3)

?

Can I write

f(r.Read(buf) or raise err)

?

defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2) or raise(err3) Transform(err3)

No, this would be invalid because of the second raise. If that wasn't there, then then the whole transformation chain should go through and the final result should be returned to the caller. Though such semantics as a whole are probably not needed, since you can just write:

defer f.Close() or raise(err2) Transform(errors.ReplaceIfNil(err, err2)


f(r.Read(buf) or raise err)

If we assume my original comment - where or would take the last value of the left side, so that if it was a default value, the final expression would evaluate to the rest of the result list; then yes, this should be valid. In this case, if r.Read returns an error, that error is returned to the caller. Otherwise, n would be passed to f

Edit:

Unless I'm getting confused by the terms, I think of or as a binary operator, whose operands have to be of the same type (but a bit magical, if the left operand is a list of things, and in that case it takes the last element of said list of things). raise would be a unary operator that takes its operand, and returns from the function, using the value of that operand as the value of the last return argument, with the preceding ones having default values. You could then techincally use raise in a standalone statement, for the purposes of returning from a function, a.k.a. return ..., err

This will be the ideal case, but I'm also fine with or raise being just a syntax alternative to =?, as long as it also accepts a simple statement instead of a block, so as to cover the majority of use cases in a less verbose manner. Or we can go with a defer-like grammar as well, where it accepts an expression. This would cover the majority of cases like:

f := os.Open("/some/file") or raise(err) errors.Wrap(err, "with context")

and complex cases:

f := os.Open or raise(err) func() {
     if err == io.EOF {
         […]
     }
  return err
}()

Thinking about my proposal a little more, I am dropping the bit about union/sum-types. The syntax I'm proposing is

guard [ ASSIGNMENT || EXPRESSION ] else { [ BLOCK ] }

In the case of an expression, the expression is evaluated and if the result is not equal to true for boolean expressions or the blank value for other expressions, BLOCK is executed. In an assignment, the last assigned value is evaluated for != true/!= nil. Following a guard statement, any assignments made will be in scope (it doesn't create a new block scope [except maybe for the last variable?]).

In Swift, the BLOCK for guard statements must contain one of return, break, continue, or throw. I haven't decided if I like that or not. It does seem to add some value because a reader knows from the word guard what will follow.

Does anyone follow Swift well enough to say if guard is well regarded by that community?

Examples:

guard f, err := os.Open("/some/file") else { return errors.Wrap(err, "could not open:") }

guard data, err := ioutil.ReadAll(f) else { return errors.Wrap(err, "could not read:") }

var obj interface{}

guard err = json.Unmarshal(data, &obj) else { return errors.Wrap(err, "could not unmarshal:") }

guard m, _ := obj.(map[string]interface{}) else { return errors.New("unexpected data format") }

guard val, _ := m["key"] else { return errors.New("missing key") }

Imho everybody is discussing too wide range of problems here at once, but the most common pattern in reality is "return error as is". So why not approach the most problem with smth like:

code, err ?= fn()

which means the function should return on err != nil?

for := operator we can introduce ?:=

code, err ?:= fn()

situation with ?:= seems to be worse due to shadowing, since compiler will have to pass variable "err" to the same named err return value.

I'm actually pretty excited that some people are focusing on making it easier to write correct code instead of merely shortening incorrect code.

Some notes:

An interesting "experience report" from one of designers of Midori at Microsoft on the error models.

I think some ideas from this document and Swift can apply beautifully to Go2.

Introducing a new reseved throws keyword, functions can be defined like:

func Get() []byte throws {
  if (...) {
    raise errors.New("oops")
  }

  return []byte{...}
}

Trying to call this function from another, non-throwing function will result in compilation error, because of unhandled throwable error.
Instead we should be able to propagate error, which everyone agree is a common case, or handle it.

func ScrapeDate() time.Time throws {
  body := Get() // compilation error, unhandled throwable
  body := try Get() // we've been explicit about potential throwable

  // ...
}

For cases when we know that a method will not fail, or in tests, we can introduce try! similar to swift.

func GetWillNotFail() time.Time {
  body := Get() // compilation error, throwable not handled
  body := try Get() // compilation error, throwable can not be propagated, because `GetWillNotFail` is not annotated with throws
  body := try! Get() // works, but will panic on throws != nil

  // ...
}

Not sure about these though (similar to swift):

func main() {
  // 1:
  do {
    fmt.Printf("%v", try ScrapeDate())
  } catch err { // err contains caught throwable
    // ...
  }

  // 2:
  do {
    fmt.Printf("%v", try ScrapeDate())
  } catch err.(type) { // similar to a switch statement
    case error:
      // ...
    case io.EOF
      // ...
  }
}

ps1. multiple return values func ReadRune() (ch Rune, size int) throws { ... }
ps2. we can return with return try Get() or return try! Get()
ps3. we can now chain calls like buffer.NewBuffer(try Get()) or buffer.NewBuffer(try! Get())
ps4. Not sure about annotations (easy way to write errors.Wrap(err, "context"))
ps5. these are actually exceptions
ps6. biggest win is compile time errors for ignored exceptions

Suggestions you write are exactly described in Midori link with all the bad
sides of it... And one obvious consequence from "throws" will be "people
hate it". Why should one write "throws" every time for most of the
functions?

BTW, your intention to force errors to be checked and not ignored can be
applied to non-error types as well and imho better to have in a more
generalized form (e.g. gcc __attribute__((warn_unused_result))).

As for the form of the operator I would suggest either short form or
keyword form like this:

?= fn() OR check fn() -- propagate error to the caller
!= fn() OR nofail fn() -- panic on error

On Sat, Aug 26, 2017 at 12:15 PM, nvartolomei notifications@github.com
wrote:

Some notes:

An interesting experience report
http://joeduffyblog.com/2016/02/07/the-error-model/ from one of
designers of Midori at Microsoft on the error models.

I think some ideas from this document and Swift
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html
can apply beautifully to Go2.

Introducing a new reseved throws keyword, functions can be defined like:

func Get() []byte throws {
if (...) {
raise errors.New("oops")
}

return []byte{...}
}

Trying to call this function from another, non-throwing function will
result in compilation error, because of unhandled throwable error.
Instead we should be able to propagate error, which everyone agree is a
common case, or handle it.

func ScrapeDate() time.Time throws {
body := Get() // compilation error, unhandled throwable
body := try Get() // we've been explicit about potential throwable

// ...
}

For cases when we know that a method will not fail, or in tests, we can
introduce try! similar to swift.

func GetWillNotFail() time.Time {
body := Get() // compilation error, throwable not handled
body := try Get() // compilation error, throwable can not be propagated, because GetWillNotFail is not annotated with throws
body := try! Get() // works, but will panic on throws != nil

// ...
}

Not sure about these though (similar to swift):

func main() {
// 1:
do {
fmt.Printf("%v", try ScrapeDate())
} catch err { // err contains caught throwable
// ...
}

// 2:
do {
fmt.Printf("%v", try ScrapeDate())
} catch err.(type) { // similar to a switch statement
case error:
// ...
case io.EOF
// ...
}
}

ps1. multiple return values func ReadRune() (ch Rune, size int) throws {
... }
ps2. we can return with return try Get() or return try! Get()
ps3. we can now chain calls like buffer.NewBuffer(try Get()) or buffer.NewBuffer(try!
Get())
ps4. Not sure about annotations


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/21161#issuecomment-325106225, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AICzv9CLN77RmPceCqvjXVE_UZ6o7JGvks5sb-IYgaJpZM4Oi1c-
.

I think the operator proposed by @jba and @bcmills is a very nice idea, although it reads better spelled as "??" instead of "=?" IMO.

Looking at this example:

func doStuff() (int,error) {
    x, err := f() 
    if err != nil {
        return 0, wrapError("f failed", err)
    }

    y, err := g(x)
    if err != nil {
        return 0, wrapError("g failed", err)
    }

    return y, nil
}

func doStuff2() (int,error) {
    x := f()  ?? (err error) { return 0, wrapError("f failed", err) }
    y := g(x) ?? (err error) { return 0, wrapError("g failed", err) }
    return y, nil
}

I think doStuff2 is considerable easier and quicker to read because it:

  1. wastes less vertical space
  2. is easy to quickly read the happy path on the left side
  3. is easy to quickly read the error conditions on the right side
  4. has no err variable polluting the function's local namespace

To me this proposal alone looks incomplete and has too much magic. How would be ?? operator defined? “Captures last return value if non-nil”? “Captures last error value if matches method type?”

Adding new operators for handling return values based on their position and type looks like a hack.

On 29 Aug 2017, 13:03 +0300, Mikael Gustavsson notifications@github.com, wrote:

I think the operator proposed by @jba and @bcmills is a very nice idea, although it reads better spelled as "??" instead of "=?" IMO.
Looking at this example:
func doStuff() (int,error) {
x, err := f()
if err != nil {
return 0, wrapError("f failed", err)
}

   y, err := g(x)
   if err != nil {
           return 0, wrapError("g failed", err)
   }

   return y, nil

}

func doStuff2() (int,error) {
x := f() ?? (err error) { return 0, wrapError("f failed", err) }
y := g(x) ?? (err error) { return 0, wrapError("g failed", err) }
return y, nil
}
I think doStuff2 is considerable easier and quicker to read because it:

  1. wastes less vertical space
  2. is easy to quickly read the happy path on the left side
  3. is easy to quickly read the error conditions on the right side
  4. has no err variable polluting the function's local namespace


You are receiving this because you commented.
Reply to this email directly, view it on GitHub, or mute the thread.

@nvartolomei

How would be ?? operator defined?

See https://github.com/golang/go/issues/21161#issuecomment-319434101 and https://github.com/golang/go/issues/21161#issuecomment-320758279.

Since @bcmills recommended resurrecting a quiescent thread, if we're going to consider cribbing from other languages, it seems like statement modifiers would offer a reasonable solution to all this. To take @slvmnd's example, redone with statement modifiers:

func doStuff() (int, err) {
        x, err := f()
        return 0, wrapError("f failed", err)     if err != nil

    y, err := g(x)
        return 0, wrapError("g failed", err)     if err != nil

        return y, nil
}

Not quite as terse as having the statement and error check in a single line, but it reads reasonably well. (I'd suggest disallowing the := form of assignment in the if expression, otherwise the scoping issues would likely confuse people even if they're clear int he grammar) Allowing "unless" as negated version of "if" is a bit of syntactic sugar, but it works nicely to read and would be worth considering.

I wouldn't recommend cribbing from Perl here, though. (Basic Plus 2 is fine) That way lies looping statement modifiers which, while sometimes useful, bring in another set of fairly complex issues.

a shorter version:
return if err != nil
should also be supported then.

with such syntax the question arises - should non-return statements be also
supported with such "if" statements, like this:
func(args) if condition

maybe instead of inventing after-action-if it's worth introducing single
line if's?

if err!=nil return
if err!=nil return 0, wrapError("failed", err)
if err!=nil do_smth()

it seems much more natural then special forms of syntax, no? Though I guess
it introduces a lot of pain in parsing :/

But... it's all just small tweaks and not special lang support for error
handling/propagating.

On Mon, Sep 18, 2017 at 4:14 PM, dsugalski notifications@github.com wrote:

Since @bcmills https://github.com/bcmills recommended resurrecting a
quiescent thread, if we're going to consider cribbing from other languages,
it seems like statement modifiers would offer a reasonable solution to all
this. To take @slvmnd https://github.com/slvmnd's example, redone with
statement modifiers:

func doStuff() (int, err) {
x, err := f()
return 0, wrapError("f failed", err) if err != nil

  y, err := g(x)
    return 0, wrapError("g failed", err)     if err != nil

    return y, nil

}

Not quite as terse as having the statement and error check in a single
line, but it reads reasonably well. (I'd suggest disallowing the := form of
assignment in the if expression, otherwise the scoping issues would likely
confuse people even if they're clear int he grammar) Allowing "unless" as
negated version of "if" is a bit of syntactic sugar, but it works nicely to
read and would be worth considering.

I wouldn't recommend cribbing from Perl here, though. (Basic Plus 2 is
fine) That way lies looping statement modifiers which, while sometimes
useful, bring in another set of fairly complex issues.


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/21161#issuecomment-330215402, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AICzv1rfnXeGVRRwaigCyyVK_STj-i83ks5sjmylgaJpZM4Oi1c-
.

Thinking some more about @dsugalski's suggestion, it does not have the property that @jba and others have requested, namely that non-error code be visibly distinct from error code. Could still be an interesting idea if it has significant benefits for non-error code paths too, but the more I think about it the less appealing it seems compared to the proposed alternatives.

I'm not sure how much visual distinction it's reasonable to expect from pure text. At some point it seems more appropriate to punt that to the IDE or code coloring layer of your text editor.

But for text-based visible distinction, the formatting standard we had in place when I first started using this an embarrassingly long time ago was that IF/UNLESS statement modifiers had to be right-justified, which made them stand out nicely enough. (Though granted a standard that was easier to enforce and perhaps more visually distinct on a VT-220 terminal than in editors with more flexible window sizes)

For me, at least, I do find that the statement modifier case is easily distinct and reads better than the current if-block scheme. This may not be the case for other people, of course -- I read source code the same way I read English text so it maps into an existing comfortable pattern, and not everyone does this.

return 0, wrapError("f failed", err) if err != nil can be written if err != nil { return 0, wrapError("f failed", err) }

if err != nil return 0, wrapError("f failed", err) can be written the same.

Maybe all that's needed here is for gofmt to leave leave if's written on a single line on a single line instead of expanding them to three lines?

There's another possibility that strikes me. A lot of the friction that I experience when trying to write throwaway Go code quickly is because I have to error check on every single call, so I can't nest calls nicely.

For example, I cannot call http.Client.Do on a new request object without first assigning the http.NewRequest result to a temporary variable, then calling Do on that.

I wonder if we could allow:

f(y())

to work even if y returns (T, error) tuple. When y returns an error, the compiler could abort the expression evaluation and cause that error to be returned from f. If f doesn't return an error, it could be given one.

Then I could do:

n, err := http.DefaultClient.Do(http.NewRequest("DELETE", "/foo", nil))

and the error result would be non-nil if either NewRequest or Do failed.

This has one significant problem, however - the above expression is already valid if f accepts two arguments, or variadic arguments. Also, the exact rules for doing this are likely to be quite involved.

So in general, I don't think I like it (I'm not keen on any of the other proposals in this thread either), but thought I'd throw the idea out for consideration anyway.

@rogpeppe or you can just use json.NewEncoder

@gbbr Ha yes, bad example.

A better example might be http.Request. I've changed the comment to use that.

Wow. A lot of ideas are making code readability even worse.
I'm ok with approach

if val, err := DoMethod(); err != nil {
   // val is accessible only here
   // some code
}

Only one thing is really annoying is scoping of returned variables.
In this case you have to use val but it's in scope of if.
So you have to use else but linter will be against it (and me too), and only way is

val, err := DoMethod()
if err != nil {
   // some code
}
// some code with val

Would be nice to have access to variables out of if block:

if val, err := DoMethod(); err != nil {
   // some code
}
// some code with val

@dmbreaker That's essentially what Swift's guard clause is for. It assigns a variable within the current scope if it passes some condition. See my earlier comment.

I'm all for simplifying the error handling in Go (although I personally don't mind it that much), but I think this adds a bit of wizardry to an otherwise simple and extremely easy to read language.

@gbbr
What is the 'this' you are referring to here? There's quite a few different suggestions on how to go about things.

Perhaps a two-part solution?

Define try as "peel off the rightmost value in the return tuple; if it is not the zero value for its type, return it as the rightmost value of this function with the others set to zero". This makes the common case

 a := try ErrorableFunction(b)

and enables chaining

 a := try ErrorableFunction(try SomeOther(b, c))

(Optionally, make it non-nil rather than non-zero, for efficiency.) If the erroring functions return a non-nil/non-zero, the function "aborts with a value". The rightmost value of the try'ed function must be assignable to the rightmost value of the calling function or it is a compile-time type checking error. (So this is not hard-coded to handling only error, though perhaps the community should discourage its use for any other "clever" code.)

Then, allow try returns to be caught with a defer-like keyword, either:

catch func(e error) {
    // whatever this function returns will be returned instead
}

or, more verbosely perhaps but more inline with how Go already works:

defer func() {
    if err := catch(); err != nil {
        set_catch(ErrorWrapper{a, "while posting request to server"})
    }
}()

In the catch case, the function's parameter must exactly match the value being returned. If multiple functions are provided, the value will pass through all of them in reverse order. You can of course put a value in that resolves to a function of the correct type. In the case of the defer-based example, if one defer func calls set_catch the next defer func will get that as its value of catch(). (If you are silly enough to set it back to nil in the process, you will get a confusing return value out. Don't do that.) The value passed to set_catch must be assignable to the returned type. In both cases I expect this to work like defer in that it is a statement, not a declaration, and will only apply to code after the statement executes.

I tend to prefer the defer-based solution from a simplicity perspective (basically no new concepts introduced there, it's a second type of recover() rather than a new thing), but acknowledge it may have some performance issues. Having a separate catch keyword could allow for more efficiency by being easier to entirely skip when a normal return occurs, and if one wished to go for maximum efficiency, perhaps tie them to scopes so that only one is allowed to be active per scope or function, which would, I think, be nearly zero-cost. (Possibly the source code file name and line number should be returned from the catch function, too? It's cheap at compile time to do that and would dodge some of the reasons people call for a full stack trace right now.)

Either would also allow repetitive error handling to be effectively handled in one place within a function, and allow error handling to be offered as a library function easily, which is IMHO one of the worst aspects of the current case, per rsc's comments above; the laboriousness of error handling tends to encourage "return err" rather than correct handling. I know I struggle with that a lot myself.

@thejerf Part of Ian's point with this proposal is to explore ways to address error boilerplate without discouraging functions from adding context or otherwise manipulating the errors they return.

Separating the error-handling into try and catch seems like it would run against that goal, although I suppose that depends on what kind of detail programs will typically want to add.

At the very least, I would want to see how it works out with examples that are more realistic.

The entire point of my proposal is to allow adding context or manipulating the errors, in a way I consider more programmatically correct than most of the proposals here which involve repeating that context over and over, which itself inhibits the desire to put the additional context in.

To rewrite the original example,

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

comes out as

func Chdir(dir string) error {
    catch func(e error) {
        return &PathError{"chdir", dir, e}
    }

    try syscall.Chdir(dir)
    return nil
}

except that this example is too trivial for, well, any of these proposals, actually, and I'd say in this case we'd just leave the original function alone.

Personally I don't consider the original Chdir function a problem in the first place. I'm tuning this specifically to address the case where a function is noisied up by lengthy repeated error handling, not for a one-error function. I'd also say that if you have a function where you are literally doing something different for every possible use case, that the right answer is probably to keep writing what we've already got. However, I suspect that is by far the rare case for most people, on the grounds that if that _was_ the common case, there wouldn't be a complaint in the first place. The noise of checking the error is only significant precisely because people want to do "mostly the same thing" over and over in a function.

I also suspect most of what people want would be met with

func SomethingBigger(dir string) (interface{}, error) {
     catch func (e error, filename string, lineno int) {
         return PackageSpecificError{e, filename, lineno, dir}
     }

     x := try Something()

     if x == true {
         try SomethingElse()
     } else {
         a, b = try AThirdThing()
     }

     return whatever, nil
}

If we shave away the problem of trying to make a single if statement look good on the grounds that it's too small to care about, and shave away the problem of a function that is truly doing something unique for every error return on the grounds that A: that's actually a fairly rare case and B: in that case, the boilerplate overhead is actually not so significant vs. the complexity of the unique handling code, the maybe the problem can be reduced to something that has a solution.

I also really want to see

func packageSpecificHandler(f string) func (err error, filename string, lineno int) {
    return func (err error, filename string, lineno int) {
        return &PackageSpecificError{"In function " + f, err, filename, lineno}
    }
}

 func SomethingBigger(dir string) (interface{}, error) {
     catch packageSpecificHandler("SomethingBigger")

     ...
 }

or some equivalent be possible, for when that works.

And, of all the proposals on the page... doesn't this still look like Go? It looks more like Go than current Go does.

To be honest, most of my professional engineering experience has been with PHP (I know) but the main attraction for Go was always the readability. While I do enjoy some aspects of PHP, the piece I despise the most is the "final" "abstract" "static" nonsense and applying overcomplicated concepts to a piece of code that does one thing.

Seeing this proposal gave me immediate flashback to the feeling of looking at a piece and having to do a double take and really "think" about what that piece of code is saying/doing. I do not think that this code is readable and does not really add to the language. My first instinct is to look on the left and I think this always returns nil. However, with this change I would now have to look left, and right, to determine the behavior of the code, which means more time reading and more mental model.

However, this does not mean there is no room for improvements to error handling in Go.

I'm sorry I haven't (yet) read this entire thread (it's super long) but I see people throwing out alternative syntax, so I'd like to share my idea:

a, err := helloWorld(); err? {
  return fmt.Errorf("helloWorld failed with %s", err)
}

Hope I didn't miss something above that nullifies this. I promise I'll get through all the comments some day :)

The operator would have to be allowed only on the error type, I believe, to avoid the type conversion semantic mess.

Interesting, @buchanae , but does it get us much over:

if a, err := helloWorld(); err != nil {
  return fmt.Errorf("helloWorld failed with %s", err)
}

I do see that it would allow a to escape, whereas in the current state, it's scoped to the then and else blocks.

@object88 You're right, the change is subtle, aesthetic, and subjective. Personally, all I want from Go 2 on this topic is a subtle readability change.

Personally, I find it more readable because the line doesn't start with if and doesn't require the !=nil. The variables are on the left edge where they are on (most?) other lines.

Great point on the scope of a, I hadn't considered that.

Considering the other possibilities of this grammar, it seems this is possible.

err := helloWorld(); err? {
  return fmt.Errorf("error: %s", err)
}

and probably

helloWorld()? {
  return fmt.Errorf("hello world failed")
}

which are maybe where it falls apart.

Maybe returning an error should be a part of every function call in Go, so you could imagine:
```
a := helloWorld(); err? {
return fmt.Errorf("helloWorld failed: %s", err)
}

What about having real exception handling? I mean Try, catch, finally instead like many modern languages?

Nope, it makes code implicit and unclear (though really shorter a bit)

On Thu, 23 Nov 2017 at 07:27, Kamyar Miremadi notifications@github.com
wrote:

What about having real exception handling? I mean Try, catch, finally
instead like many modern languages?


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/21161#issuecomment-346529787, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AICzvyy_kGAlcs6RmL8AKKS5deNRU4_5ks5s5PQVgaJpZM4Oi1c-
.

Returning to @mpvl's WriteToGCS example up-thread, I'd like to suggest (again) that the commit/rollback pattern isn't common enough to warrant a major change in Go's error handling. It's not hard to capture the pattern in a function (playground link):

func runWithCommit(f, commit func() error, rollback func(error)) (err error) {
    defer func() {
        if r := recover(); r != nil {
            rollback(fmt.Errorf("panic: %v", r))
            panic(r)
        }
    }()
    if err := f(); err != nil {
        rollback(err)
        return err
    }
    return commit()
}

Then we can write the example as

func writeToGCS(ctx context.Context, bucket, dst string, r io.Reader) error {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    return runWithCommit(
        func() error { _, err := io.Copy(w, r); return err },
        func() error { return w.Close() },
        func(err error) { _ = w.CloseWithError(err) })
}

I would suggest simpler solution:

func someFunc() error {
    ^err := someAction()
    ....
}

For multiple multiple function returns:

func someFunc() error {
    result, ^err := someAction()
    ....
}

And for multiple return arguments:

func someFunc() (result Result, err error) {
    var result Result
    params, ^err := someAction()
    ....
}

^ sign means return if parameter is not nil.
Basically "move error up the stack if it happens"

Any drawbacks of this method?

@gladkikhartem
How would one modify the error before it is returned?

@urandom
Wrapping errors is an important action that in my opinion should be done explicitly.
Go code is about readability, not magic.
I would like to keep error wrapping more clear

But at the same time I would like to get rid of the code that doesn't carry lots of information and just takes the space.

if err != nil {
    return err
}

It's like a Go's cliche - you don't want to read it, you want to just skip it.

What I have seen so far in this discussion is a combination of:

  1. reducing syntax verbosity
  2. improving the error by adding context

This is in line with the original issue description by @ianlancetaylor that mentions both aspects, however in my opinion the two should be discussed/defined/experimented separately and possibly in different iterations to limit the scope of changes and just for reasons of effectiveness (a bigger change to the language is more difficult to do rather than an incremental one).

1. Syntax verbosity reduction

I like the idea of @gladkikhartem, even in its original form that I report here since it was edited/extended:

 result, ^ := someAction()

In the context of a func:

func getOddResult() (int, error) {
    result, ^ := someResult()
    if result % 2 == 0 {
          return result + 1, nil
    }
    return result, nil
}

This short syntax - or in the form proposed by @gladkikhartem with err^ - would address the syntax verbosity part of the problem (1).

2. Error context

For the 2nd part, adding more context, we could even completely forget about it for now and later on propose to automatically add a stacktrace to each error if a special contextError type is used. Such new native error type could sport full or short stacktraces (imagine a GO_CONTEXT_ERROR=full) and be compatible with the error interface while offering the possibility to extract at least the function and filename from the top call stack entry.

When using a contextError, somehow Go should attach the call stacktrace at exactly the point where the error is created.

Again with a func example:

func getOddResult() (int, contextError) {
    result, ^ := someResult() // here a 'contextError' is created; if the error received from 'someResult()' is also a `contextError`, the two are nested
    if result % 2 == 0 {
          return result + 1, nil
    }
    return result, nil
}

Only the type changed from error to contextError, which could be defined as:

type contextError interface {
    error
    Stack() []StackEntry
    Cause() contextError
}

(notice how this Stack() is different than https://golang.org/pkg/runtime/debug/#Stack, since we would hope to have a non-bytes version of the goroutine call stack here)

The Cause() method would return nil or the previous contextError as a result of nesting.

I am very well aware of the potential memory implications of carrying around stacks like this, hence I hinted at the possibility of having a default short stack that would contain only 1 or few more entries. A developer would usually enable full stracktraces in development/debug versions and leave the default (short stacktraces) otherwise.

Prior art:

Just food for thought.

@gladkikhartem @gdm85

I think you have missed the point of this proposal. Per Ian's original post:

It is already easy (perhaps too easy) to ignore the error (see #20803). Many existing proposals for error handling make it easier to return the error unmodified (e.g., #16225, #18721, #21146, #21155). Few make it easier to return the error with additional information.

Returning errors unmodified is often wrong, and usually at the very least unhelpful. We want to encourage careful error handling: addressing only the “return unmodified” use case would bias the incentives in the wrong direction.

@bcmills if context (in form of a stack trace) is being added then the error is returned with additional information. Would attaching a human-readable message e.g. "error while inserting record" be considered "careful error handling"? How to decide at which point in the call stack such messages should be added (in each function, top/bottom etc)? These are all common questions while coding error handling improvements.

The "return unmodified" could be counteracted as explained above with "return unmodified with stacktrace" by default, and (in a reactive style) add a human readable message as needed. I have not specified how such human readable message could be added, but one can see how wrapping works in pkg/errors for some ideas.

"Returning errors unmodified is often wrong": hence I propose an upgrade path for the lazy use case, which is the same use case currently pointed out as detrimental.

@bcmills
I 100% agree with #20803 that errors should be always handled or explicitly ignored (and i have no idea why this wasn't done before...)
yes, I did not address the point of proposal and I don't have to. I care about actual solution proposed, not intentions behind it, because intentions do not match the outcomes. And when I see || such || stuff being proposed - it makes me really sad.

If embedding info, such as error codes and error messages into error would be easy and transparent - you won't need to encourage careful error handling - people will do that themselves.
For example just make error an alias. We could return any kind of stuff and use it outside function without casting. Would make life so much easier.

I love that Go reminds me to handle errors, but I hate when design encourages me to so something that is questionable.

@gdm85
Adding stack trace to an error automatically is a terrible idea, just look and Java stack traces.
When you wrap errors yourself - it's much more easier to navigate and understand what's going wrong. That's the whole point of wrapping it.

@gladkikhartem I disagree that a form of "automatic wrapping" would be much worse to navigate and in helping to understand what's going wrong. I also do not get exactly what you refer to in Java stack traces (I guess of exceptions? aesthetically ugly? what specific problem?), but to discuss in a constructive direction: what could be a good definition of "carefully handled error"?

I ask both to enhance my understanding of Go best practices (the more or less canonical that they might be) and because I feel like such definition might be key to make some proposal towards an improvement from current situation.

@gladkikhartem I know this proposal is all over the place already, but let's please do what we can to keep it focused on the goals I set out initially. As I said when posting this issue, there are already several different proposals that are about simplifying if err != nil { return err }, and those are the place to discuss syntax that only improves that specific case. Thanks.

@ianlancetaylor
Sorry if I moved discussion out of it's way.

If you want to add context information to an error I would suggest using this syntax:
(and enforce people to use only one error type for one function for easy context extraction)

type MyError struct {
    Type int
    Message string
    Context string
    Err error
}

func VeryLongFunc() error {
    var err MyError
    err.Context = "general function context"


   result, ^err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
   }

    // in case we need to make a cleanup after error

   result, ^err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       file.Close()
   }

   // another variant with different symbol and return statement

   result, ?err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       return err
   }

   // using original approach

   result, err.Err := someAction()
   if err != nil {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       return err
   }
}

func main() {
    err := VeryLongFunc()
    if err != nil {
        e := err.(MyError)
        log.Print(e.Error(), " in ", e.Dir)
    }
}

^ symbol is used to indicate error parameter, as well as differentiate function definition from error handling for "someAction() { }"
{ } could be omitted if error is returned unmodified

Adding some more resources to reply to my own invite to better define "careful error handling":

As tedious as the current approach is, I think it's less confusing than the alternatives, although one line if statements might work? Maybe?

blah, err := doSomething()
if err != nil: return err

...or even...

blah, err := doSomething()
if err != nil: return &BlahError{"Something",err}

Someone might have brought this up already, but there are many, many posts and I've read many of them but not all. That said, I personally think it would be better to be explicit than to be implicit.

I have been a fan of railway oriented programming, the idea come from Elixir's with statement.
else block will be executed once e == nil short circuited.

Here is my proposal with pseudo code ahead:

func Chdir(dir string) (e error) {
    with e == nil {
            e = syscall.Chdir(dir)
            e, val := foo()
            val = val + 1
            // something else
       } else {
           printf("e is not nil")
           return
       }
       return nil
}

@ardhitama Isn't this then like Try catch except "With" is like "Try" statement and that "Else" is like"Catch"?
Why not go implementing exception handling like Java or C# ?
right now in go if a programmer doesn't want to handle exception in that function, it returns it as a result of that function. Still there is no way to force a programmers to handle an exception if they don't want to and many times you really don't need to, but what we get here are lots of if err!=nil statements that makes the code ugly and not readable(lots of noise). Isn't it the reason Try Catch Finally statement was invented in the first place in other programming language?

So, I think it is better if Go Authors "Try" to not be stubborn!! and just introduce the "Try Catch Finally" statement in the next versions. Thank you.

@KamyarM
You cannot introduce exception handling in go, because are no exceptions in Go.
Introducing try{} catch{} in Go is like introducing try{} catch{} in C - it's just totally wrong.

@ianlancetaylor
What about not changing Go error handling at all, but rather change gofmt tool like this for single-line error handling?

err := syscall.Chdir(dir)
    if err != nil {return &PathError{"chdir", dir, err}}
err = syscall.Chdir(dir2)
    if err != nil {return err}

It's backward compatible and you could apply it to your current projects

Exceptions are decorated goto statements, they turn your call stack into a call graph and there's a good reason most serious non-academic projects bar or limit their use. A stateful object calls a method that transfers control arbitrarily up the stack and then resumes executing instructions... sounds like a bad idea because it is.

@KamyarM In essence it is, but In practice it is not. In my opinion because we are being explicit here and not breaking any Go idioms.

Why?

  1. Expressions inside with statement can not declare new var, hence it explicitly states the intention is to evaluate out of block vars.
  2. Statements inside with will behave like inside try and catch block. Indeed it will be slower as on each next instruction it need to evaluates with's conditions in worst case scenario.
  3. By design the intention is to remove excessive ifs and not to create exception handler as the handler will always local (the with's expression and else block).
  4. No need of stack unwinding because of throw

ps. please correct me if I'm wrong.

@ardhitama
KamyarM is right in a sense that with statement looks as ugly as try catch and it also introduces indentation level for normal code flow.
Not even mentioning original proposal's idea to modify each error individually. It's just not going to work elegantly with try catch, with or any other method that groups statements together.

@gladkikhartem
Yes, hence I propose to adopt "railway oriented programming" instead and try not remove the explicitness. It's just another angle to attack the problem, the other solutions want to solve it by not letting the compiler automagically write if err != nil for you.

with also not just for error handling, it can be useful for any other control flow.

@gladkikhartem
Let me make it clear I think Try Catch Finally block is beautiful. If err!=nil ...is actually the ugly code.

Go is just a programming language. There are so many other languages. I found out that many in the Go Community look at it like their religion and are not open to change or admit the mistakes. This is wrong.

@gladkikhartem

I am ok if Go Authors call it Go++ or Go# or GoJava and introduce The Try Catch Finally there ;)

@KamyarM

Avoiding unnecessary change is necessary--critical--for any engineering endeavor. When people say change in this context, they really mean _change for the better_, which they convey effectively with _arguments_ guiding a premise to that intended conclusion.

The _just open your mind man!_ appeal isn't convincing. Ironically, it tries to say something most programmers view as ancient and clunky is _new and improved_.

There are also many proposals and discussions where the Go community discusses previous mistakes. But I do agree with you when you say Go is just a programming language. It does says so on the Go website and other places, and I've spoken to a few people who confirmed it as well.

I found out that many in the Go Community look at it like their religion and are not open to change or admit the mistakes.

Go is based on academic research; personal opinions don't matter.

Even lead developers of Microsoft's C# compiler acknowledged publicly that _exceptions_ are a bad way to manage errors, while prising Go/Rust model as a better alternative: http://joeduffyblog.com/2016/02/07/the-error-model/

Surely, there is room for improving Go's error model, but not by adopting exceptions-like solutions since they only add a ginormous complexity in exchange for a few questionable benefits.

@Dr-Terrible Thank you for the article.

But I didn't find anywhere that mentions GoLang as an academic language.

Btw, to make my point clear, in this example

func Execute() error {
    err := Operation1()
    if err!=nil{
        return err
    }

    err = Operation2()
    if err!=nil{
        return err
    }

    err = Operation3()
    if err!=nil{
        return err
    }

    err = Operation4()
    return err
}

Is similar to implement exception handling in C# like this:

         public void Execute()
        {

            try
            {
                Operation1();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation2();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation3();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation4();
            }
            catch (Exception)
            {
                throw;
            }
        }

Isn't that a terrible way of exception handling in C#? My answer is yes, I don't know about yours! In Go I have no other choice. It is that terrible choice or highway. This is how it is in GO and I don't have choice.

By the way as also mentioned in the article you shared, any language can implement error handling like Go without any need for any extra syntax so Go actually didn't implement any revolutionary way of error handling. It just doesn't have any way of error handling and so you are limited to using If statement for error handling.

Btw, I know GO has a non recommended Panic, Recover , Defer that is kind of similar to Try Catch Finally but in my personal opinion Try Catch Finally syntax is much cleaner and better organized way of exception handling.

@Dr-Terrible

Also please check this out:
https://github.com/manucorporat/try

@KamyarM , he didn't say that Go is an academic language, he said it's based on academic research. Nor was the article about Go, but it investigates the error handling paradigm employed by Go.

If you find that manucorporat/try works for you, then please do use it in your code. But the costs (performance, language complexity, etc.) of adding try/catch to the language itself are not worth the tradeoff.

@KamyarM
You're example is not accurate. Alternative to

    err := Operation1()
    if err!=nil {
        return err
    }
    err = Operation2()
    if err!=nil{
        return err
    }
    err = Operation3()
    if err!=nil{
        return err
    }
    return Operation4()

will be

            Operation1();
            Operation2();
            Operation3();
            Operation4();

exception handling seems much better option in this example. In theory it should be good, but in practice
you have to reply with accurate error message for each error occurred in your endpoint.
The whole application in Go is usually 50% error handling.

         err := Operation1()
    if err!=nil {
        log.Print("input error", err)
                fmt.Fprintf(w,"invalid input")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation2()
    if err!=nil{
        log.Print("controller error", err)
                fmt.Fprintf(w,"operation has no meaning in this context")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation3()
    if err!=nil{
        log.Print("database error", err)
                fmt.Fprintf(w,"unable to access database, try again later")
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }

And if people will have such a powerful tool as try catch I'm 100% sure they will overuse it in favor of careful error handling.

It's interesting that academia is mentioned, but Go is a collection of lessons learned from practical experience. If the goal is to write a bad API that returns incorrect error messages, exception handling is the way to go.

However, I don't want an invalid HTTP header error when my request contains a malformed JSON request body, exception handling is the magical fire-and-forget button that achieves that in the C++ and C# APIs that use them.

For large API coverage, it is impossible to provide enough error context to achieve meaningful error handling. That's because any good application is 50% error handling in Go, and should be 90% in a language that requires a non-local control transfer to handle errors.

@gladkikhartem

The Alternative way you mentioned is the right way to write the code in C#. It is just 4 lines of the code and shows the happy execution path. It doesn't have those if err!=nil noises. If an exception happens the function that cares about those exception can handle it using Try Catch Finally (It can be the same function itself or the caller or the caller of the caller or the caller of the caller of the caller of the caller ... or just an event handler that process all the unhandled errors in an application. The programmer has different choices.)

err := Operation1()
    if err!=nil {
        log.Print("input error", err)
                fmt.Fprintf(w,"invalid input")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation2()
    if err!=nil{
        log.Print("controller error", err)
                fmt.Fprintf(w,"operation has no meaning in this context")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation3()
    if err!=nil{
        log.Print("database error", err)
                fmt.Fprintf(w,"unable to access database, try again later")
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }

Looks simple but that is a tricky one. I guess, you could throw together a custom error type which carries the system error, the user error (without leaking internal state to the user who might not have the best of intentions), and the HTTP code.

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

But give it a try

func Chdir(dir string) error {
    return  syscall.Chdir(dir) ? &PathError{"chdir", dir, err}:nil;
}
func Chdir(dir string) error {
    return  syscall.Chdir(dir) ? &PathError{"chdir", dir, err};
}



md5-9bcd2745464e8d9597cba6d80c3dcf40



```go
func Chdir(dir string) error {
    n , _ := syscall.Chdir(dir):
               // something to do
               fmt.Println(n)
}

All of those contain some kind of un-obvious magic, which does not simplify things for the reader. In the former two examples, err becomes some sort of pseudo keyword or spontaneously occurring variable. In the latter two examples, it's not at all clear what that : operator is supposed to be doing -- is an error going to get automatically returned? Is the RHS of the operator a single statement, or a block?

FWIW, I'd write a wrapper function so that you could do return newPathErr("chdir", dir, syscall.Chdir(dir)) and it would automatically return a nil error if the third parameter is nil. :-)

IMO, the best proposal I have seen to achieve goals of "simplify error handling in Go" and "return the error with additional contextual information" is from @mrkaspa in #21732:

a, b, err? := f1()

expands to this:

if err != nil {
   return nil, errors.Wrap(err, "failed")
}

and I can force it to panic with this:

a, b, err! := f1()

expands to this:

if err != nil {
   panic(errors.Wrap(err, "failed"))
}

This will keep backward compatibility, and will mend all the pain points of error handling in go

This doesn't handle cases like bufio functions that return non-zero values as well as errors but I think it's okay to do explicit error handling in cases where you care about the other return values. And of course the non-error return values would need to be the appropriate nil value for that type.

The ? modifier will reduce boilerplate error handing in functions and the ! modifier will do the same for places where assert would be used in other languages such as in some main functions.

This solution has the advantage of being very simple and not trying to do too much, but I think meets the requirements laid out in this proposal statement.

In the case where you have...

func foo() (int, int, error) {
    a, b, err? := f1()
    return a, b, nil
}
func bar() (int, error) {
    a, b, err? := foo()
    return a+b, nil
}

If something goes wrong in foo, then at bar's call site, the error is doubly wrapped with the same text, without adding any meaning. At the very least, I would object to the errors.Wrap part of the suggestion.

But expanding further, what is the expected result of this?

func baz() (a, b int, err error) {
  a = 1
  b = 2
  a, b, err? = f1()
  return

Are a and b reassigned to nil values? If so, that's magic, which I feel that we should avoid. Do they carry out the values previously assigned? (I don't care for named return values myself, but they should still be considered for the purpose of this proposal.)

@dup2X Yea let's remove the language, it should be more that way

@object88 it's only natural to expect that in case of an error everything else is nulled. That's simple to understand and has no magic in it, pretty much already a convention for Go code, requires little to remember and has no special cases. If we allow values to be retained then it complicates things. If you forget to check for error, returned values could be used by accident in which case anything could happen. Like calling methods on partially allocated structure instead of panicking on nil. Programmers might even start expecting certain values to be returned in case of an error. In my opinion, it would be a mess and nothing good would be gained from it.

As for wrapping, I don't think default message provides anything useful. It would be fine to just chain errors together. Like when exceptions have inner exceptions in them. Very useful for debugging errors deep inside a library.

Respectfully, I disagree, @creker. We have examples of that scenario in the Go stdlib of non-nil return values even in the case of a non-nil error, and in fact are functional, such as several functions in the bufio.Reader struct. We as Go programmers are actively encouraged to check / handle all errors; it feels more egregious to ignore errors than to get non-nil returns values & an error. In the case you cite, if you return a nils and don't check the error, you may still operating on an invalid value.

But setting that aside, let's examine this a little further. What would be the semantics of the ? operator? Can it only be applied to types that implement the error interface? Can it be applied to other types or return arguments? If it can be applied to types that don't implement error, is it trigger by any non-nil value / pointer? Can the ? operator be applied to more than one return value, or is that a compiler error?

@erwbgy
If you one want to just return error without anything useful attached to it - it would be much simpler to just tell compiler to treat all unhanded errors as "if err != nil return...", for example:

func doStuff() error {
    doAnotherStuff() // returns error
}

func doStuff() error {
    res := doAnotherStuff() // returns string, error
}

And there's no need for additional crazy ? symbol in this case.

@object88
I've tried to apply most of the error-wrapping proposals shown here in real code and faced one major issue - the code becomes too dense and unreadable.
What it does is just sacrifices code width in favor of code height.
Wrapping errors with usual if err != nil actually allows to spread the code for better readability, so I don't think we should even change anything for error wrapping at all.

@object88

In the case you site, if you return a nils and don't check the error, you may still operating on an invalid value.

But that will produce obvious and easy to spot error like panic on nil. If you do need to return meaningful values on error you should do it explicitly and document exactly which value is usable in which case. Just returning random stuff that happened to be in the variables upon error is dangerous and will lead to subtle errors. Again, nothing is gained from that.

@gladkikhartem the problem with if err != nil is that actual logic is completely lost in it and you have to actively search for it if you want to understand what the code does on its successful path and don't care about all that error handling. It becomes like reading a lot of C code where you have several lines of actual code and everything else is just error checking. People even resort to macroses that wrap all of that and goto to the end of the function.

I don't see how logic can become too dense in properly written code. It's logic. Every line of your code contains actual code you care about, that's what you want. What you don't want is to go through lines and lines of boilerplate. Use comments and split your code into blocks if that helps. But that sounds more like a problem with actual code and not the language.

This works in the playground, if you don't reformat it:

a, b, err := Frob("one string"); if err != nil { return a, b, fmt.Errorf("couldn't frob: %v", err) }
// continue doing stuff with a and b

So it seems to me that the original proposal, and many of the others mentioned above, are trying to come up with a clear shorthand syntax for those 100 characters, and stop gofmt from insisting on adding linebreaks and reformatting the block across 3 lines.

So let's imagine we change gofmt to stop insisting on multi-line blocks, start with the line above, and try to come up with ways to make it shorter and clearer.

I don't think the part before the semicolon (the assignment) should be changed, so that leaves 69 characters we might cut down. Of those, 49 are the return statement, the values to return, and the error wrapping, and I don't see much value in changing the syntax of that (say, by making return statements optional, which confuses users).

So that leaves finding a shorthand for ; if err != nil { _ } where underscore represents a chunk of code. I think any shorthand should explicitly include err for clarity even if it makes the nil comparison somewhat invisible, so we're left with coming up with a shorthand for ; if _ != nil { _ }.

Imagine for a moment that we use a single character. I'm going to pick § as a placeholder for whatever that character might be. The line of code would then be:

a, b, err := Frob("one string") § err return a, b, fmt.Errorf("couldn't frob: %v", err)

I don't see how you could do much better than that without either changing existing assignment syntax or return syntax, or having invisible magic happen. (There's still some magic, in that the fact that we're comparing err to nil isn't readily apparent.)

That's 88 characters, saving a grand total of 12 characters on a line 100 characters long.

So my question is: Is it really worth doing?

Edit: I guess my point is, when people look at Go's if err != nil blocks and say "I wish we could get rid of that crap", 80-90% of what they're talking about is _stuff you inherently have to do in order to handle errors_. The actual overhead caused by Go's syntax is minimal.

@lpar, you're mostly following the same logic I applied above, so naturally I agree with your reasoning. But I think you discount the visual appeal of putting all the error stuff to the right:

a, b := Frob("one string")  § err { return ... }

is more readable by a factor that exceeds the mere reduction in characters.

@lpar you can save even more characters if you remove pretty much useless fmt.Errorf, change return to some special syntax and introduce call stack to errors so that they have actual context to them and not being a mere glorified strings. That would leave you with something like this

a, b, err? := Frob("one string")

The problem with Go errors for me was always a lack of context. Returning and wrapping strings is not useful at all to determine where the error is actually happened. That's why github.com/pkg/errors for example, became a must for me. With errors like that I get the benefits of Go error handling simplicity and the benefits of exceptions that perfectly capture context and allow you to find the exact place of failure.

And, even if we take your example as it is, the fact that error handling is on the right is a significant readability upgrade. You no longer have to skip several lines of boilerplate to get to the actual meaning of the code. You can say what you want about importance of error handling but when I read code in order to understand it I don't care about errors. All I need is successful path. And when I do need errors I will look for them specifically. Errors, by their nature, are exceptional case and should take as little space as possible.

I think the question of whether fmt.Errorf is "useless" compared to errors.Wrap is orthogonal to this issue, as they're both about equally verbose. (In actual applications I don't use either, I use something else which also logs the error and the line of code on which it occurred.)

So I guess it comes down to some people really liking the error handling being on the right. I'm just not that convinced, even coming from a Perl and Ruby background.

@lpar I use errors.Wrap because it automatically captures call stack - I don't really need all these error messages. I care more about the place where it happened and, maybe, which arguments were passed to the function that produced the error. You even saying that you're doing similar thing - log line of code to provide some context to your error messages. Given that we can think of ways of reducing boilerplate while giving errors more context (that's pretty much the proposal here).

As for errors being on the right. For me it's not simply about the place but about reducing cognitive load that it takes to read a code littered with error handling. I don't buy the argument that errors are so important that you want them take as much space as they do. I'd actually prefer them to go away as much as possible. They're not the main story.

@creker

This is more likely describing a trivial developer error than an error in a production system generating an error due to bad user input. If all you need to determine the error is the line number and file path, chances are you just wrote the code and already know whats wrong.

@as it's similar to exceptions. In most of the cases call stack and exception message is enough to nail down the place and cause where error happened. In more complex cases you at least know the place where error happened. Exceptions give you this benefit by default. With Go you either have to chain errors together, basically emulating call stack, or include the actual call stack.

In properly written code most of the time you would know from line number and file path the exact cause because the error would be expected. You actually wrote some code in preparation that it might happen. If something not expected happened then yes, call stack would not give you the cause but it would significantly reduce search space.

@as

In my experience, user input errors are handled almost immediately. The real problematic production errors happen deep in the code (e.g. a service is down, causing another service to throw errors), and its quite useful to get a proper stack trace. For what it's worth, the java stack traces are extremely useful when debugging production problems, not the messages.

@creker
Errors are just values, and they are part of the function inputs and outputs. They could not be "unexpected".
If you want to find out why function gave you an error - use testing, logging and etc...

@gladkikhartem in the real world it's not that simple. Yes, you expect errors in a sense that function signature includes an error as its return value. But what I ment by expecting is knowning exactly why it happened and what caused it, so you actually know what to do to fix it or not to fix it at all. Bad user input is usually really simple to fix just by looking at the error message. If you use profocol buffers and some required field is not set, that's expected and really simple to fix if you properly validate everything you receive on the wire.

At this point I no longer understand what we're arguing about. Stack trace or chain of error messages are pretty similar if implemented propertly. They reduce search space and provide you useful context to reproduce and fix an error. What we need is to think about ways of simplifying error handling while providing enough context. I'm in no way advocating that simplicity is more important than proper context.

That's the Java argument — move all the error code somewhere else so you don't have to look at it. I think it's misguided; not just because Java's mechanism for doing so has largely failed, but because when I'm looking at code, how it will behave when there's an error is as important to me as how it will behave when everything works.

No one is making that argument. Let's not confuse what's being discussed here with exception handling where all error handling is in one place. Calling it "largely failed" is just an opinion but I don't think Go will ever return to that in any case. Go error handling is just different and can be improved.

@creker I tried to make the same point and asked to clarify what is considered a meaningful/useful error message.

Truth is, I would give away any day a variable-quality error message text (which has the bias of the developer writing it in that moment and with that knowledge) in exchange for the call stack and function arguments. With an error message text (fmt.Errorf or errors.New) you end up searching the text in the source code, while reading call stacks/backtraces (which are seemingly hated and I hope not for aesthetic reasons) corresponds to looking up directly by file/line number (errors.Wrap and similar).

Two different styles, but the purpose is the same: trying to reproduce in your mind what happened at runtime in those conditions.

On this topic, issue #19991 is perhaps making a valid summary for an approach to the second style of defining meaningful errors.

move all the error code somewhere else so you don't have to look at it

@lpar, if you're responding to my point about moving error-handling to the right: there's a big difference between footnotes/endnotes (Java) and side-notes (my proposal). Side notes require only a tiny eye shift, without loss of context.

@gdm85

you end up searching the text in the source code

Exactly what I meant by stack traces and chained error messages being similar. They both record a path that it took to the error. Only in case of messages you could end up with completely useless messages that could be from anywhere in the program if you're not careful enough writing them. Only benefit of chained errors is the ability to record values of variables. And even that could be automated in case of function arguments or even variables in general and would, at least for me, cover almost everything I need from errors. They would still be values, you can still type switch them if you need. But at some point you would probably log them and being able to see stack trace is extremely useful.

Just look at what Go does with panics. You get full stack trace of every goroutine. I can't remember how many times it helped me nail down the cause of the error and fix it in no time. It often amazed me how easy it is. It flows perfectly with the whole language being very predictable that you don't even need debugger.

There seems to be stigma around everything related to Java and people often don't bring any arguments. It's bad just because. I no fan of Java but that kind of reasoning is not helping anybody.

Again, errors are not for the developer to fix bugs. That is one benefit of error handling. The Java way has taught developers that that's what error handling is, it not. Errors can exist at the application layer and beyond that at a flow layer. Errors in Go are routinely used to control the recovery strategy a system takes--at runtime, not compile time.

This might be incomprehensible when languages cripple their flow control as a result of an error by unraveling the stack and losing memory of everything they did before the error occurred. Errors are actually useful at runtime in Go; I don't see why they should carry things like line numbers--running code hardly cares about that.

@as

Again, errors are not for the developer to fix bugs

That's completely and utterly wrong. Errors are exactly for that reason. They're not limited to it but it's one of the main uses. Errors indicate that's something wrong with the system and you should do something about it. For expected and easy errors you can try to recover like, for example, TCP timeout. For something more serious you dump it in logs and later debug the issue.

That is one benefit of error handling. The Java way has taught developers that that's what error handling is, it not.

I don't know what Java taught you but I use exceptions for the same reason - to control recovery strategy system takes at runtime. Go has nothing special about it in terms of error handling.

This might be incomprehensible when languages cripple their flow control as a result of an error by unraveling the stack and losing memory of everything they did before the error occurred

Might be for someone, not for me.

Errors are actually useful at runtime in Go; I don't see why they should carry things like line numbers--running code hardly cares about that.

If you care about fixing bugs in your code then line numbers is the way to do it. It's not Java that taught us about this, C has __LINE__ and __FUNCTION__ exactly for that reason. You want to log your errors and record the exact place where it happened. And when something goes wrong you at least has something to start from. Not a random error message that was caused by an unrecoverable error. If you don't need that kind of information then ignore it. It doesn't hurt you. But at least it's there and can be used when needed.

I don't understand why people here keep shifting conversation into exceptions vs error values. No one was making that comparison. Only thing that was discussed is that stack traces are very useful and carry a lot of context information. If that's incomprehensible then you probably live in a completely different universe where tracing does not exist.

That's completely and utterly wrong.

But the production system I'm referring to is still running, and it uses errors for flow control, is written in Go, and replaced a slower implementation in a language that used stack traces for error propagation.

If that's incomprehensible then you probably live in a completely different universe where tracing does not exist.

To chain around call stack information for every function that returns an error type, do that at your discretion. Stack traces are slower, and unsuitable for use outside toy projects for security reasons. It's a technical foul to make them first class Go citizens to merely aid thoughtless error propagation strategies.

if you don't need that kind of information then ignore it. It doesn't hurt you.

Software bloat is the reason servers are rewritten in Go. What you don't see can still degrade the throughput of your pipeline.

I would prefer examples of actual software that benefits from having this feature instead of a mildly-irrelevant lesson on handling TCP timeouts and log dumping.

Stack traces are slower

Given that the stack traces are generated in the error path, nobody cares how slow they are. The normal operation of the software was already interrupted.

and unsuitable for use outside toy projects for security reasons

So far I have yet to see a single production system turn off stack traces due to "security reasons", or at all for that matter. On the other hand, being able to quickly identify the path the code took to produce an error has been extremely useful. And this is for big projects, with many different teams working on the code base, and no one having full knowledge of the whole system.

What you don't see can still degrade the throughput of your pipeline.

No, it really doesn't. As I said before, stack traces are generated on errors. Unless your software is constantly encountering them, throughput will not be affected one bit.

Given that the stack traces are generated in the error path, nobody cares how slow they are. The normal operation of the software was already interrupted.

Untrue.

  • Errors can occur as a part of normal operation.
  • Errors can be recovered from and the program can continue, so performance is still in question.
  • Slowing down one routine saps resources from other routines that _are_ operating in the happy path.

@object88 imagine real production code. How much errors you expect it to generate? I would think not much. At least in a properly written application. If a goroutine is in a busy loop and constantly throws errors on each iteration, there's something wrong with the code. But even if that's the case, given that wast majority of Go applications are IO bound even that would not be a serious problem.

@as

But the production system I'm referring to is still running, and it uses errors for flow control, is written in Go, and replaced a slower implementation in a language that used stack traces for error propagation.

I'm sorry but this is a nonsensical sentence that has nothing to do with what I said. Not gonna answer it.

Stack traces are slower

Slower but how much? Does it matter? I don't think so. Go applications are IO bound in general. Chasing around CPU cycles is silly in this case. You have much bigger problems in Go runtime which eats up CPU. It's not an argument to throw away useful feature that helps fixing bugs.

unsuitable for use outside toy projects for security reasons.

I'm not gonna bother covering non-existent "security reasons". But I'd like to remind you that usually application traces stored internally and only developers have access to them. And trying to hide your functions names is waste of time anyway. It's not security. I hope I don't need to elaborate on that.

If you insist on security reasons I would like you to think about macOS/iOS, for example. They have no problem throwing panics and crash dumps that contain stacks of all threads and values of all CPU registers. Don't see them being affected by these "security reasons".

It's a technical foul to make them first class Go citizens to merely aid thoughtless error propagation strategies.

Could you be any more subjective? "thoughtless error propagation strategies" where did you see that?

Software bloat is the reason servers are rewritten in Go. What you don't see can still degrade the throughput of your pipeline.

Again, by how much?

I would prefer examples of actual software that benefits from having this feature instead of a mildly-irrelevant lesson on handling TCP timeouts and log dumping.

At this point it seems I'm talking with anyone but a programmer. Tracing benefits any and all software. It's a common technique in all languages and all types of software that helps fixing bugs. You can read wikipedia if you would like more information on that.

Having so many unproductive discussions without any consensus means there's no elegant way to solve this issue.

@object88
Stack traces could be slow if you want to take trace of all goroutines, because Go should wait for other goroutines to unblock.
If you just trace goroutine you're currently running - it's not that slow.

@creker
Tracing benefits all software, but it depends on what you are tracing. In most of the Go projects I was involved tracing stacks was not a great idea, because concurrency is involved. Data is moving back an forth, lot's of stuff is communicating with each other and some goroutines are a just few lines of code. Having stack trace in such case makes no help.
That's why I use errors wrapped with context information written to log to recreate same stack trace, but which is bound not to the actual goroutine stack, but the application logic itself.
So that I could just do cat *.log | grep "orderID=xxx" and get the stack trace of the actual sequence of actions that led to an error.
Due to concurrent nature of Go context-rich errors are more valuable than stack traces.

@gladkikhartem thank you for taking the time to write a proper argument. I was starting to get frustrated with that conversation.

I understand your argument and partly agree with it. Still, I find myself having to deal with stacks of at least 5 functions deep. That's already big enough to be able to understand what's going on and where you should start looking. But in a highly concurrent application with lots of very small goroutines stack traces loose their benefits. That I agree with.

@creker

imagine real production code. How much errors you expect it to generate? [...], given that wast majority of Go applications are IO bound even that would not be a serious problem.

Good that you mention IO bound operations. The io.Reader Read method returns a healthy error at EOF. So that's going to happen in the happy path a lot.

@urandom

Stack traces involuntarily expose information valuable for profiling a system.

  • User names
  • File system paths
  • Backend database type/version
  • Transaction flow
  • Object structure
  • Encryption algorithms

I don't know if the average application would notice the overhead of collecting stack frames in error types, but I can tell you that for performance critical applications a lot of small Go functions are manually inlined because of the current function call overhead. Tracing will make it worse.

I believe the goal for Go is to have simple and fast software, and tracing would be a step back. We should be able to write small functions and return errors from those functions without performance degradation that encourages unconventional error types and manual in-lining.

@creker

I will avoid giving you examples that cause further dissonance. I'm sorry that I frustrated you.

I would propose using a new keyword "returnif" which the name reveals instantly its function. Also it is flexible enough that it could to be used in more use cases than error handling.

Example 1 (using named return):

a, err = something(b)
if err != nil {
return
}

Would become:

a, err = something(b)
returnif err != nil

Example 2 (Not using named return):

a, err := something(b)
if err != nil {
return a, err
}

Would become:

a, err := something(b)
returnif err != nil { a, err }

Regarding your named return example, do you mean...

a, err = something(b)
returnif err != nil

@ambernardino
Why not just update fmt tool instead and you don't have to update a language syntax and add new, useless keywords

a, err := something(b)
if err != nil { return a, err }

or

a, err := something(b)
    if err != nil { return a, err }

@gladkikhartem the idea is not to type that every time that you want to propagate the error, I'd rather this and should work the same way

a, err? := something(b)

@mrkaspa
The idea is to make code more readable. Typing code is not a problem, reading is.

@gladkikhartem rust uses that approach and I don't think it makes it less readable

@gladkikhartem I don't think ? makes it any less readable. I would say it eliminates the noise completely. The problem for me is that with the noise it also eliminates the possibility to provide useful context. I just don't see where you could plug in the usual error message or wrap errors. Call stack is an obvious solution but, as already been mentioned, does not work for everyone.

@mrkaspa
And I think that it makes it less readable, what's next? We are trying to find best solution or just sharing opinions?

@creker
'?' character adds cognitive load on reader, because it's not so obvious what is going to be returned and of course person should know what this stuff it doing. And of course ? sign raises questions in reader's minds.

As I said previously, if you want to get rid of err != nil compiler could detect unused error parameters and forward them himself.
And

a, err? := doStuff(a,b)
err? := doAnotherStuff(b,z,d,g)
a, b, err? := doVeryComplexStuff(b)

will become more readable

a := doStuff(a,b)
doAnotherStuff(b,z,d,g)
a, b := doVeryComplexStuff(b)

same magic, just less stuff to type and less stuff to think about

@gladkikhartem well, I don't think there's a solution that would not require readers to learn something new. That's the consequence of changing the language. We have to make a trade off - either we live with verbose into your face syntax that shows exactly what's being done in primitive terms or we introduce new syntax that could hide verbosity, add some syntax sugar etc. There's no other way. Just rejecting anything that's adds something for reader to learn is counter-productive. We might as well just close all Go2 issues and call it a day.

As for your example, it introduces even more magic stuff and hides any injection point to introduce syntax that would allow developer to provide context to errors. And, most important, it completely hides any information about which function call might throw an error. That smells more and more like exceptions. And if we're serious about just re-throwing errors then stack traces becomes a must because that's the only way you can retain context in that case.

The original proposal actually covers all that pretty well already. It verbose enough and gives you a good place to wrap error and provide useful context. But one of the main problems is this magic 'err'. I think it's ugly because it's not magic enough and not verbose enough. It's kind of in the middle. What might make it better is to introduce more magic.

What if || would produce a new error that automagically wraps original error. So the example becomes like this

func Chdir(dir string) error {
    syscall.Chdir(dir) || &PathError{"chdir", dir}
    return nil
}

err just disappears and all wrapping is handled implicitly. Now we need some way to access those inner errors. Adding another method like Inner() error to error interface will not work I think. One way is to introduce built-in function like unwrap(error) []error. What it does is returns a slice of all inner errors in the order they were wrapped. That way you can access any inner error or range over them.

The implementation of this is questionable given that error is just an interface and you need a place where to put those wrapped errors for any custom error type.

For me this ticks all the boxes but might be somewhat too magical. But, given that error interface is pretty special by definition, bringing it closer to first-class citizen might not be a bad idea. Error handling is verbose because it's just a regular Go code, there's nothing special about it. That might be good on paper and for making flashy headlines but errors are too special to warrant such treatment. They need special casing.

Is the original proposal about reducing the number of error checks or the length of each individual check?

If its the latter then its trivial to refute the proposals necessity on the grounds that there is only one conditional statement and one repitition statement. Some people dislike for loops, should we provide an implicit loop construct too?

The proposed syntax changes so far have served as an interesting thought experiment but none of them are as clear, pronounceable, or simple as the original. Go is not bash and nothing should be "magic" about errors.

More and more I read these proposals, more and more I see people which arguments are nothing but "it adds something new, so it's bad, unreadable, leave everything as it is".

@as the proposal gives the summary of what it tries to achieve. What's being done is pretty well defined.

as clear, pronounceable, or simple as the original

Any proposal will introduce new syntax and being new for some people sounds the same as "unreadable, complicated etc etc". Just because it's new doesn't make it any less clear, pronounceable or simple. "||" and "?" examples are as clear and simple as the existing syntax once you know what it does. Or should we start complaining that "->" and "<-" are too magical and reader has to know what they mean? Let's replace them with method calls.

Go is not bash and nothing should be "magic" about errors.

That's completely unfounded and does not count as argument for anything. What's that got to do with Bash is beyond me.

@creker
Yes, i totally agree with you that introduced event more magic. My example is just a continuation of ? operator idea of typing less stuff.

I agree that we need to sacrifice something and introduce some change and of course some magic. It's just a balance of pros of usability and cons of such magic.

Original || proposal is pretty nice, and practically-tested, but the formatting is ugly in my opinion, i'd suggest changing formatting to

syscal.Chdir(dir)
    || return &PathError{"chdir", dir}

P.S. what you think about such variant of magic syntax?

syscal.Chdir(dir) {
    return &PathError{"chdir", dir}
}

@gladkikhartem both look pretty good from readability standpoint but I have a bad feeling from the latter. It introduces this strange block scope I'm not so sure about.

I'd encourage you to not look at the syntax in isolation, but rather in context of a function. This method has a couple of different error handling blocks.

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
        absPath, err := p.preparePath()
        if err != nil {
                return nil, err
        }
    fis, err := l.context.ReadDir(absPath)
    if err != nil {
        return nil, err
    } else if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }

    return buildPkg, nil
}

How do the proposed changes clean this function up?

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
    absPath, err? := p.preparePath()
    fis, err? := l.context.ReadDir(absPath)
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
         if _, ok := err.(*build.NoGoError); ok {
             // There isn't any Go code here.
             return nil, nil
         }
         return nil, err
    }

    return buildPkg, nil
}

The @bcmills proposal fits better.

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
    absPath := p.preparePath()        =? err { return nil, err }
    fis := l.context.ReadDir(absPath) =? err { return nil, err }
    if len(fis) == 0 {
        return nil, nil
    }
    buildPkg := l.context.Import(".", absPath, 0) =? err {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }
    return buildPkg, nil
}

@object88

func (l *Loader) processDirectory(p *Package) (p *build.Package, err error) {
        absPath, err := p.preparePath() {
        return nil, fmt.Errorf("prepare path: %v", err)
    }
    fis, err := l.context.ReadDir(absPath) {
        return nil, fmt.Errorf("read dir: %v", err)
    }
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0) {
        err, ok := err.(*build.NoGoError)
                if !ok {
            return nil, fmt.Errorf("buildpkg: %v",err)
        }
        return nil, nil
    }
    return buildPkg, nil
}

Continuing to poke at this. Maybe we can come up with more complete examples of usage.

@erwbgy, this one looks the best to me, but I'm not convinced that the payout is that great.

  • What are the non-error return values? Are they always the zero-value? If there _was_ a previously assigned value for a named return, does that get overridden? If the named value gets used to store the result of an erroring function, does that get returned?
  • Can the ? operator be applied to a non-error value? Could I do something like (!ok)? or ok!? (which is a little weird, because you're bundling assignment and operation)? Or is this syntax only good for error?

@rodcorsi, this one bugs me because what if the function was not ReadDir but ReadBuildTargetDirectoryForFileInfo or something silly long like that. Or perhaps you have a large number of arguments. The error handling for preparePath would also get pushed way off screen. On a device with limited horizontal screen size (or viewport that isn't that wide, like Github), you are likely to lose the =? part. We are very good at vertical scrolling; not so much at horizontal.

@gladkikhartem, it seems like it's tied to some (only the last?) argument implementing the error interface. It looks a lot like a function declaration, and that just... _feels_ weird. Is there some way that it could be tied to an ok-style return value? Overall, you're only buying 1 line.

@object88
word-wrapping solves really wide code problems. is it not used widely?

@object88 regarding very long function call. Let's deal with the main issue here. The issue is not the error handling pushed off screen. The issue is a long function name and/or a big list of arguments. That needs to be fixed before any argument can be made about error handling being off screen.

I have yet to see an IDE or code-friendly text editor that was set up to word wrap by default. And I haven't found a way to do it at all w/ Github aside from manually hacking the CSS after the page is loaded.

And I do think that code width is an important factor -- it speaks to _readability_, which is the impetus for this proposal. The claim is that there's "too much code" around errors. Not that the functionality isn't there, or that errors need to be implemented in some other way, but that the code doesn't read well.

@object88
yes, this code will work for any function returning error interface as last parameter.

Regarding line saving, you just can't put more information in less number of lines. Code should be evenly distributed, not too dense and nor having space after every statement.
I agree it looks like a function declaration, but at the same time it's very similar to existing if ...; err != nil { statement, so people won't be too confused.

Code width is an important factor. What if i have 80 line editor and 80 lines of code is a function call and after that I have || return err statement? I just won't be able to identify that function returns something, because what I'll read will be a valid go code without any returns.

Just for completeness I will throw an example with || syntax, my automagic error wrapping and auto-zeroing of non-error return values

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
        absPath := p.preparePath() || errors.New("prepare path")
    fis := l.context.ReadDir(absPath) || errors.New("ReadDir")
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }

    return buildPkg, nil
}

Regarding your question about other return values. In case of an error they will have zero value in all cases. I already covered why I believe that's important.

The problem is your example is not that involved to begin with. But it still shows what this proposal, at least for me, represents. What I want it to solve is the most common and wildly used idiom

err := func()
if err != nil {
    return err
}

We can all I agree that kind of code is a big part (if not the biggest) of error handling in general. So it only logical to solve that case. And if you want to do something more involved with the error, apply some logic - go for it. That's where verbosity should be where there's an actual logic for programmer to read and understand. What we don't need is to waste space and time reading mindless boilerplate. It's mindless but still essential part of Go code.

Regarding earlier talk about implicitly returning zero values. If you need to return meaningful value on error - go for it. Again, verbosity is good here and helps understanding code. Nothing wrong with ditching syntactic sugar if you need to do something more complicated. And || is flexible enough to solve both cases. You can leave out non-error values and they will be zeroed implicitly. Or you can specify them explicitly if you need to. I remember there's even a separate proposal for this that also involves cases where you want to return error and zero out everything else.

@object88

The claim is that there's "too much code" around errors.

It's not just any code. The main issue is that there's too much meaningless boilerplate around errors and very common case of error handling. Verbosity important when there's something of a value to read. There's nothing of value in if err == nil then return err except that you want to rethrow the error. For such a primitive logic it takes to much space. And more you have logic, library calls, wrappers etc that all could very well return an error, the more this boilerplate starts dominating important stuff - the actual logic of your code. And that logic can actually contain some important error handling logic. But it gets lost in this repetitive nature of most of the boilerplate around it. And that can be solved and other modern languages that Go competes with try to solve that. Because error handling is that important, it's not just a regular code.

@creker
I agree that if err != nil return err is too much boilerplate, the thing we are afraid is that if we'll create easy way to just forward error up the stack - statistically programmers, especially juniors will use the easiest method, rather doing what is appropriate in a certain situation.
It's the same idea with Go's error handling - it forces you to do a decent thing.
So in this proposal we want to encourage others to handle and wrap errors thoughtfully.

I would say that we should make simple error handling look ugly and long to implement, but graceful error handling with wrapping or stack traces look nice and easy to do.

@gladkikhartem I always found this argument about ancient editors silly. Who cares about that and why the language should suffer from this? It's 2018, pretty much everyone has big display and some decent editor. The very very small minority should not influence everyone else. It should be the other way around - the minority should deal with it by themselves. Scroll, word wrapping, anything.

@gladkikhartem Go already has that problem and I don't think we can do anything about that. Developers will always be lazy until you force them with failed compilation or runtime panic which, by the way, Go is not doing.

What Go actually does is not to force anything. The notion that Go forces you to handle errors is misleading and always was. Go authors force you to do that in their blog posts and conference talks. The actual language leaves you to do anything you want. And the main problem here is what Go chooses by default - by default error is silently ignored. There's even proposal to change that. If Go was about forcing you to do a decent thing then it should do the following. Either return an error at compile time or panic at runtime if returned error is not handled properly. From what I understand Rust does this - errors panic by default. That's what I call forcing to do the right thing.

What actually forced me to handle errors in Go is my developer conscience, nothing else. But it's always tempting to give in. Right now if I don't explicitly go about reading function signature no one will tell me anything that it returns an error. There's an actual example. For a long time I had no idea that fmt.Println returned an error. I have no use for its return value, I just want to print stuff. So there's not incentive for me to look at what it returns. That's the same problem C has. Errors are values and you can ignore them all you want until your code breaks at runtime and you won't know anything about it because there's no crash with helpful panic like, for example, with unhandled exceptions.

@gladkikhartem from what I understand about this proposal, it's not to encourage developers to wrap errors thoughtfully. It's about encouraging those who will come up with proposals to not forget to cover that. Because often people come up with solutions that just rethrow error and forget that you actually want to give it more context and only then rethrow it.

I'm writing this proposal mainly to encourage people who want to simplify Go error handling to think about ways to make it easy to wrap context around errors, not just to return the error unmodified.

@creker
My editor is 100 character width, because I have file explorer, git console and etc.... , in our team nobody writes a code more than 100 character length, it's just silly ( with a few exceptions )

Go is not forcing error handling, linters do. ( Maybe we should just write a linter for this? )

Ok, if we can't come up with solution and everybody understands proposal in his own way - why not specify some requirements for thing we need? kind of agree on requirements first and then developing solution would be much easier task.

For example:

  1. New proposal's syntax should have return statement in text, otherwise it's not obvious for reader what's happening. ( agree / disagree )
  2. New proposal should support functions that return multiple values ( agree / disagree )
  3. New proposal should take less space ( 1 line, 2 lines, disagree)
  4. New proposal should be able to handle very long expressions ( agree / disagree)
  5. New proposal should allow multiple statements in case of error ( agree / disagree)
  6. .....

@creker, about 75% of my development is done on a 15" laptop in VSCode. I maximize my horizontal real estate, but there's still a limit, especially if I'm side-by-side editting. I would wager that among students, there are far more laptops than desktops. I would not want to limit the approachability of the language because we anticipate everyone to have large-format monitors.

And unfortunately, no matter how large a screen you have, github still limits the viewport.

@gladkikhartem

Novice laziness is applicable here, but the liberal usage of errors.New in some of these examples also demonstrates a lack of understanding of the language. Errors shouldn't be allocated in return values unless they're dynamic, and if those errors would be put in a comparable package scoped variable, the syntax would be shorter on the page and actually acceptable in production code too. Those who suffer the most from Go error handling "boilerplate" take the most shortcuts and do not have enough experience to handle errors properly.

It is not obvious what constitutes simplifying error handling, but the precedent is that less runes != simple. I think there are a few qualifiers for simplicity that can measure a construct in a quantifiable way:

  • The number of ways that construct is expressed
  • The similarity of that construct to other construct, and the cohesion between those constructs
  • The number of logical operations summarized by the construct
  • The similarity of the construct to natural language (i.e., absence of negation, etc)

For example, the original proposal increases the number of ways to propagate errors from 2 to 3. It's similar to the logical OR, but has different semantics. It summarizes a conditional return of low complexity (compared to say copy or append, or >> ). The new method is less natural than the old, and if spoken out loud would probably be abs, err := path(foo) || return err -> if theres an error, it's returning err in which case it would be a mystery why it's possible to use the vertical bars if you can write it the same way its said out loud in a code review.

@as
Totally agree that less runes != simple .
By simple i mean readable and understandable.
So that anyone who is not familiar with go should read it and understand what it does.
It should be like a joke - you don't have to explain it.

Current error handling actually is understandable, but not completely readable if you have too much if err != nil return.

@object88 that's okay. I said more in general because this argument comes up fairly frequently. Like, let's imagine some ridiculous ancient terminal screen that could be used for writing Go. What kind of argument is that? Where is the limit of it's ridiculousness? If we're serious about it then we should observe hard facts - what's the most popular screen size and resolution. And only from that we can draw something. But argument usually just imagines some screen size that nobody uses but there's a small possibility somebody could.

@gladkikhartem no, linters do not force you, they suggest. There's a big difference here. They're optional, not a part of Go toolchain and only make suggestions. Forcing can only mean two things - compile or runtime error. Everything else is a suggestion and option to choose from.

I agree, we should better formulate what we want because the proposal doesn't fully cover all aspects.

@as

The new method is less natural than the old, and if spoken out loud would probably be abs, err := path(foo) || return err -> if theres an error, it's returning err in which case it would be a mystery why it's possible to use the vertical bars if you can write it the same way its said out loud in a code review.

The new method is less natural only for one reason - it's not a part of the language right now. There's no other reason. Imagine Go already with that syntax - it would be natural because you're familiar with it. Just like your're familiar with ->, select, go and other thing not present in other languages. Why it's possible to use vertical bars instead of return? I answers with a question. Why there's a way to append slices in one call when you can do the same thing with loop? Why there's a way to copy stuff from reader to writer interface in one call when you can do the same with loop? etc etc etc Because you want your code to be more compact and more readable. You're making these arguments when Go already contradicts them with numerous examples. Again, let's be more open and not shot down anything just because it's new and not already in the language. We will not achieve anything with that. There's a problem, many people are calling for solution, let's deal with it. Go is not a sacred ideal language that will be desecrated by anything added to it.

Why there's a way to append slices in one call when you can do the same thing with loop?

Writing an if statement error check is trivial, I would be interested in seeing your implementation of append.

Why there's a way to copy stuff from reader to writer interface in one call when you can do the same with loop?

A reader and writer abstracts away the sources and destinations of a copy operation, the buffering strategy, and sometimes even the sentinel values in the loop. You cant express that abstraction with a loop and slice.

You're making these arguments when Go already contradicts them with numerous examples.

I don't believe that's the case, at least not with those examples.

Again, let's be more open and not shot down anything just because it's new and not already in the language.

Given that Go has a compatibility guarantee, you should be scrutinizing new features the most as you will have to deal with them forever if they're terrible. What nobody has done here so far is created an actual proof of concept and used it with a small development team.

If you look at the history of some proposals (e.g., generics), you will see that after doing just that the realization is often: "wow, this actually isn't a good solution, let's not make any changes yet". The alternative is a language full of suggestions and no easy way to retroactively evict them.

About the wide vs thin screen thing, another thing to consider is multi-tasking.

You might have multiple windows side-by-side to occasionally keep track of something else while you throw together a bit of code, rather than just staring at the editor, completely switching contexts to another window to look up a function on, perhaps StackOverflow, and jump all the way back to the editor.

@as
Totally agree that most proposed features are impractical, and I'm starting to think that || and ? stuff could be the case.

@creker
copy() and append() are not trivial tasks to implement

I have linters on CI/CD and they literally force me to handle all errors. They are not a part of language, but it don't care - I just need results.
(and by the way, i have strong opinion - if somebody's not using linters in Go - he's just ........ )

About screen size - it's not even funny, seriously. Please stop this irrelevant discussion. Your screen could be as wide as you want - you'll always have a probability that || return &PathError{Err:err} part of code won't be visible. Just google word "ide" and see what kind of space is available for code.

And please read other's text thoughtfully, I did not say that Go forces you to handle all errors

It's the same idea with Go's error handling - it forces you to do a decent thing.

@gladkikhartem Go does not force anything in terms of error handling, that's the issue. Decent thing or not, it doesn't matter, that's just nit picking. Even though for me it means handle all errors in all cases apart from maybe things like fmt.Println.

if somebody's not using linters in Go - he's just

Maybe be. But if something is not really forced then it's not gonna fly. Some will use it, others will not.

About screen size - it's not even funny, seriously. Please stop this irrelevant discussion.

I'm not the one who started throwing random numbers which should somehow affect decision making. I clearly state that I understand the problem but it should be objective. Not "I have 80 symbols wide IDE, Go should account for that and ignore everyone else".

If we're talking about my screen size. visual studio code gives me 270 symbols of horizontal space. I'm not gonna advocate that it's normal to take that much space. But my code can easily exceed 120 symbols when you take into account structs with comments and particularly long named field types. If I were to use || syntax then it would easily fit into 100-120 in case of a 3-5 argument function call and wrapped error with custom message.

Ether way if anything like || were to be implement then gofmt probably shouldn't force you to write it on one line. In some cases it may very well take too much space.

@erwbgy, this one looks the best to me, but I'm not convinced that the payout is that great.

@object88 The payout for me is that it removes common boilerplate for simple error handling and doesn't try to do too much. It only makes:

val, err := func()
if err != nil {
    return nil, errors.WithStack(err)
}

simpler:

val, err? := func()

Nothing prevents more complex error handling from being done the current way.

What are the non-error return values? Are they always the zero-value? If there was a previously assigned value for a named return, does that get overridden? If the named value gets used to store the result of an erroring function, does that get returned?

All other return parameters are appropriate nil values. For named parameters I'd expect that they'd keep any previously assigned value as they're guaranteed to be assigned some value already.

Can the ? operator be applied to a non-error value? Could I do something like (!ok)? or ok!? (which is a little weird, because you're bundling assignment and operation)? Or is this syntax only good for error?

No, I don't think it makes sense to use this syntax for anything other than error values.

I think "must" functions are going proliferate out of desperation for more readable code.

sqlx

db.MustExec(schema)

html template

var t = template.Must(template.New("name").Parse("html"))

I propose the panic operator (not sure if I should call it an 'operator')

a,  😱 := someFunc(b)

same as, but maybe more immediate than

a, err := someFunc(b)
if err != nil {
  panic(err)
}

😱 is probably too difficult to type, we could use something like !, or !!, or

a,  !! := someFunc(b)
!! = maybeReturnsError()

Maybe !! panics and ! returns

Time for my 2 cents. Why can't we just use standard library's debug.PrintStack() for stack traces? The idea is to print the stack trace only at the deepest level, where the error occurred.

Why can't we just use standard library's debug.PrintStack() for stack traces?

Errors may transit across many stacks. They can be sent across channels, stored in variables, etc. It's often more helpful to know those transition points than to know the fragment where the error was first generated.

Furthermore, the stack trace itself often includes internal (unexported) helper functions. That's handy when you're trying to debug an unexpected crash, but not helpful for errors that occur in the course of normal operation.

What is the most user friendly approach for complete programming newbies?

I am found of simpler version. Needs just one if !err
Nothing special, intuitive, no extra punctuation, vastly smaller code

```go
absPath, err := p.preparePath()
return nil, err if err

err := doSomethingWith(absPath) if !err
doSomethingElse() if !err

doSomethingRegardlessOfErr()

// Handle err in one place; if needed; catch-like without indenting
if err {
return "error without code pollution", err
}
```

err := doSomethingWith(absPath) if !err
doSomethingElse() if !err

Welcome back, good old MUMPS post conditions ;-)

Thanks, but no thanks.

@dmajkic This doesn't do anything to help with"return the error with additional contextual information".

@erwbgy the title of this issue is _proposal: Go 2: simplify error handling with || err suffix_ my comment was in that context. Sorry if I stepped in previous discussion.

@cznic Yup. Post conditions are not Go-way, but pre conditions also look polluted:

if !err; err := doSomethingWith(absPath)
if !err; doSomethingElse()

@dmajkic There is more to the proposal than just the title - ianlancetaylor describes three ways of handling errors and specifically points out that few proposals make it easier to return the error with additional information.

@erwbgy I did went through all issues specified by @ianlancetaylor They all relay on adding new keywords (like try() ) or using special non-alfanumeric chars. Personally - I don't like that, as code overloaded with !"#$%& tends to look offensive, like swearing.

I do agree, and feel, what the first few lines of this issue state: too much Go code goes on error handling. Suggestion I made is in line with that sentiment, with suggestion very close to what Go feels now, without need for extra keywords or key-chars.

How about a conditional defer

func something() (int, error) {
    var error err
    var oth err

    defer err != nil {
        return 0, mycustomerror("More Info", err)
    }
    defer oth != nil {
        return 1, mycustomerror("Some other case", oth)
    }

    _, err = a()
    _, err = b()
    _, err = c()
    _, oth = d()
    _, err = e()

    return 2, nil
}


func something() (int, error) {
    var error err
    var oth err

    _, err = a()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, err = b()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, err = c()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, oth = d()
    if oth != nil {
        return 1, mycustomerror("Some other case", oth)
    }
    _, err = e()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }

    return 2, nil
}

That would significantly change the meaning of defer -- it's just something that's run at the end of the scope, not something that causes a scope to exit early.

If they introduce the Try Catch in this language all these problems will be solved in a very easy way.

They should introduce something like this. If the value of error is set to something other than nil it can break the current workflow and automatically trigger the catch section and then the finally section and the current libraries can work too with no change. Problem solved!

try (var err error){
     i, err:=DoSomething1()
     i, err=DoSomething2()
     i, err=DoSomething3()
} catch (err error){
   HandleError(err)
   // return err  // similar to throw err
} finally{
  // Do something
}

image

@sbinet This is better than nothing but if they just simply use the same try-catch paradigm that everyone are familiar with it is much better.

@KamyarM You seem to be suggesting adding a mechanism to throw an exception whenever a variable is set to a non-zero value. That is not the "paradigm that everyone are familiar with." I'm not aware of any language that works that way.

Looks similar to Swift which also has "exceptions" that don't quite work like exceptions.

Different languages have shown that try catch is really a second class solution, while I guess Go will not be able to solve that like with a Maybe monad and so on.

@ianlancetaylor I just referred to the Try-Catch in other programming languages such as C++, Java , C# ,... and not the solution I had here. It was better if GoLang had the Try-Catch from day 1 so we didn't need to deal with this way of error handling (which was not actually new . You can write the same GoLang error handling with any other programming language if you want to code like that) but what I suggest was a way to have backward compatibility with the current libraries that can return error object.

Java exceptions are a train wreck, so I have to firmly disagree with you here @KamyarM. Just because something is familiar, does not mean that it is a good choice.

What I mean.

@KamyarM Thanks for the clarification. We explicitly considered and rejected exceptions. Errors are not exceptional; they happen for all sorts of completely normal reasons. https://blog.golang.org/errors-are-values

Exceptional or not but they do solve the issue of code bloat due to error handling boilerplate. The same issue crippled Objective-C which works pretty much exactly like Go. Errors are just values of type NSError, nothing special about them. And it has the same problem with loads of ifs and error wrapping. Which is why Swift changed things around. They ended up with a mix of two - it works like exceptions meaning that it terminates execution and you should catch the exception. But it doesn't unwind the stack and works like a regular return. So the technical argument against using exceptions for control flow doesn't apply there - these "exceptions" are just as fast as regular return. It's more of a syntactic sugar. But Swift has a unique problem with them. Many of Cocoa APIs are asynchronous (callbacks and GCD) and just not compatible with that kind of error handling - exceptions are useless without something like await. But pretty much all Go code is synchronous and these "exceptions" could actually work.

@urandom
Exceptions in Java are not bad. The problem is with bad programmers that don't know how to use it.

If your language has terrible features, someone will eventually use that feature. If your language has no such feature, there is a 0% chance. It is simple math.

@as I don't agree with you that try-catch is a terrible feature. It is very useful feature and makes our life much easier and that's the reason we are commenting here so may be Google GoLang team adds a similar functionality. I personally hate those if-elses error handling codes in GoLang and I don't like that defer-panic-recover concept that much (It is similar to try-catch but not as organized as it is with Try-Catch-Finally blocks). It adds so much noise into the code that makes the code unreadable in many cases.

The functionality to handle errors without boilerplate already exists in the language. Adding more features to satiate beginners coming from exception based languages doesn't seem like a good idea.

And what about who's coming from C/C++, Objective-C where we have the same exact problem with boilerplate? And it's frustrating to see a modern language like Go suffer from exactly the same problems. That's why this whole hype around errors as values feels so fake and silly - it's already been done for years, tens of years. It feels like Go learned nothing from that experience. Especially looking at Swift/Rust who's actually trying to find a better way. Go settled with existing solution like Java/C# settled with exceptions but at least those are much older languages.

@KamyarM Have you ever used railway oriented programming? The BEAM?

You would not praise exceptions so much, if you would use these, imho.

@ShalokShalom Not much. But isn't that just an state machine? In case of failure do this and in case of success to that? Well I think not all type of errors should handle like exceptions. When only a user input validation is needed one can just simply return a boolean value with the detail on the validation error(s). Exceptions should be limited to IO or Network access or bad function inputs and in the case an error is really critical and you want to stop the happy execution path at all costs.

One of the reason some people say Try-Catch is not good is because of its performance. Probably that is caused by using a handler map table for each place that an exception may occur. I read somewhere that even the exceptions are faster(Zero-Cost when no exceptions occur but has way more cost when they actually happen) comparing it to If Error check(It is always checked regardless of having error or not). Other than that I don't think there is any issue with Try-Catch syntax. It is only the way it is implemented by the compiler that makes it different not its syntax.

People coming from C/C++ exactly praise Go for NOT having exceptions and
for making a wise choice, resisting those who claim it is “modern” and
thanking god about readable workflow (especially after C++).

On Tue, 17 Apr 2018 at 03:46, Antonenko Artem notifications@github.com
wrote:

And what about who's coming from C/C++, Objective-C where we have the same
exact problem with boilerplate? And frustrating to see a modern language
like Go suffer from exactly the same problems. That's why this whole hype
around errors as values feels so fake and silly - it's already been done
for years, tens of years. It feels like Go learned nothing from that
experience. Especially looking at Swift/Rust who's actually trying to find
a better way. Go settled with existing solution like Java/C# settled with
exceptions but at least those are much older languages.


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/golang/go/issues/21161#issuecomment-381793840, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AICzv9w608ea2fwPq_wNpTDBnKMAdAKTks5tpTtsgaJpZM4Oi1c-
.

@kirillx I never said I want exceptions like in C++. Please read my comment again. And what that have to do with C where error handling is even more horrible? Not only you have tons of boilerplate but you also lack defer and multiple return values which forces you to return values using pointer arguments and use goto to organize your cleanup logic. Go uses the same concept of errors but solves some of the problems with defer and multiple return values. But boilerplate is still there. Other modern languages also don't want exceptions but also don't want to settle with C style because of its verbosity. Which is why we have this proposal and so much interest to this problem.

People advocating for exceptions should read this article: https://ckwop.me.uk/Why-Exceptions-Suck.html

The reason why Java/C++ style exceptions are inherently bad has nothing to do with performance of particular implementations. Exceptions are bad because they are BASIC's "on error goto", with the gotos invisible in the context where they may take effect. Exceptions hide error handling away where you can easily forget it. Java's checked exceptions were supposed to solve that problem, but in practice they didn't because people just caught and ate the exceptions or dumped stack traces everywhere.

I write Java most weeks, and I emphatically do not want to see Java-style exceptions in Go, no matter how high performance they are.

@lpar Isn't all the for loops, while loops, if elses , switch cases , breaks and continues kind of GoTo things. What is left from a programming language then?

While, for and if/else don't involve flow of execution jumping invisibly to somewhere else with no marker to indicate that it will.

What is different if someone just pass the error to the previous caller in GoLang and that caller just return that to previous caller and so on so forth (other than lots of code noise)? How much codes we need to look and traverse to see who is going to handle the error? The same goes with try-catch.

What can stop the programmer? Sometimes the function really doesn't need to handle an error. we just want to pass the error to the UI so a user or the system admin can resolve it or find a workaround.

If a function doesn't want to handle an exception it simply doesn't use try-catch block so the previous caller can handle it. I don't think the syntax has any issue. It is also much cleaner. Performance and the way it is implemented in a language is different though.

As you can see below , we need to add 4 lines of code just not to handle an error:

func myFunc1() error{
  // ...
  if (err){
      return err
  }
  return nil
}

If you want to pass errors back to the caller to handle, that's fine. The point is that it's visible that you're doing so, at the point where the error was returned to you.

Consider:

x, err := lib.SomeFunc(100, 4)
if err != nil {
  // A
}
// B

From looking at the code, you know that an error might occur when calling the function. You know that if the error occurs, code flow will end up at point A. You know that the only other place code flow will end up is point B. There's also an implicit contract that if err is nil, x is some valid value, zero or otherwise.

Contrast with Java:

x = SomeFunc(100, 4)

From looking at the code, you have no idea whether an error might occur when the function is called. If an error occurs and is expressed as an exception, then a goto happens, and you could end up somewhere at the bottom of the surrounding code... or if the exception isn't caught, you could end up somewhere at the bottom of a completely different piece of your code. Or you could end up in someone else's code. In fact, since the default exception handler can be replaced, you could potentially end up literally anywhere, based on something done by someone else's code.

Furthermore, there's no implicit contract that x is valid -- it's common for functions to return null to indicate errors or missing values.

With Java, these problems can occur with every single call -- not only with bad code, it's something you have to worry about with _all_ Java code. That's why Java development environments have pop-up help to show you whether the function you're pointing at might cause an exception or not, and which exceptions it might cause. It's why Java added checked exceptions, so that for common errors you had to at least have some warning that the function call might raise an exception and divert program flow. Meanwhile, the returned nulls and the unchecked nature of NullPointerException are such a problem that they added the Optional class to Java 8 to try and ameliorate it, even though the cost is having to explicitly wrap the return value on every single function that returns an object.

My experience is that NullPointerException from an unexpected null value I've been handed is the single most common way my Java code ends up crashing, and I usually end up with a big backtrace which is almost entirely useless, and an error message which doesn't indicate the cause because it was generated far away from the code at fault. In Go, I honestly haven't found nil dereference panics to be a significant problem, even though I'm much less experienced with Go. That, to me, indicates that Java should learn from Go, rather than the other way around.

I don't think the syntax has any issue.

I don't think anyone is saying that the syntax is the problem with Java-style exceptions.

@lpar , Why nil dereference panics in Go is better that NullPointerException in Java? What is the difference of "Panic" and "Throw"? What is the difference in their semantics?

Panics are just recover-able and throws are catch-able? Right?

I've just remembered one difference , with panic you can panic an error object or a string object or may be any other type of objects(correct me if I am wrong) but with throw you can throw an object of type Exception or a subclass of exception only.

Why nil dereference panics in Go is better that NullPointerException in Java?

Because the former almost never happen in my experience, whereas the latter happen all the time, for the reasons I explained.

@lpar Well I haven't programmed with Java recently and I guess that's a new thing(last 5 years) but C# hast safe navigation operator to avoid null references to create exceptions but What does Go have? I am not sure but I guess it doesn't have anything to handle those situations. So if you want to avoid panic you still need to add those ugly nested if-not-nil-else statements to the code.

You generally don't need to check return values to see if they're nil in Go, as long as you check the error return value. So no ugly nested if statements.

Null dereference was a bad example. If you don't catch it Go and Java work exactly the same - you get a crash with stack trace. How can stack trace be useless I don't now. You know the exact place where it happened. In both C# and Go for me it usually trivial to fix that kind of crash because null dereference in my experience is due to a simple programmer error. In this particular case there's nothing to learn from anyone.

@lpar

Because the former almost never happen in my experience, whereas the latter happen all the time, for the reasons I explained.

That's accidental and I didn't see any reason in your comment that Java somehow worse at nil/null than Go. I observed numerous nil dereference crashes in Go code. They're exactly the same as null dereference in C#/Java. You might happen to be using more value types in Go which helps (C# also has them) but doesn't change anything.

As for exceptions, let's look at Swift. You have a keyword throws for functions that could throw an error. Function without it can't throw. Implementation wise it works like return - probably some register is reserved for returning error and every time you throw function returns normally but carries with it an error value. So the problem of unexpected errors solved. You know exactly which function might throw, you know exact place where it could happen. Errors are values and don't require stack unwinding. They're just returned until you catch it.

Or something similar to Rust where you have special Result type that carries result and an error. Errors can be propagated without any explicit conditional statements. Plus a ton of pattern matching goodness but that's probably not for Go.

Both of these languages take both solutions (C and Java) and combine them to something better. Error propagation from exceptions + error values and obvious code flow from C + no ugly boilerplate code that does nothing useful. So I think it's wise to look at these particular implementation and not turn them away completely just because they resemble exceptions in some way. There's a reason exceptions are used in so many languages because they do have a positive side to them. Otherwise languages would ignore them. Especially after C++.

How can stack trace be useless I don't now.

I said "almost entirely useless". As in, I only need one line of information from it but it's dozens of lines long.

That's accidental and I didn't see any reason in your comment that Java somehow worse at nil/null than Go.

Then you're not listening. Go back and read the part about implicit contracts.

Errors can be propagated without any explicit conditional statements.

And that's exactly the problem -- errors getting propagated and control flow changing with nothing explicit to mark that it's going to happen. Apparently you don't think it's a problem, but others disagree.

Whether exceptions as implemented by Rust or Swift suffer from the same problems as Java I don't know, I'll leave that to someone experienced with the languages in question.

@KamyarM You basically make nil superfluous and get full type safety for it:

https://fsharpforfunandprofit.com/posts/the-option-type/

And that's exactly the problem -- errors getting propagated and control flow changing with nothing explicit to mark that it's going to happen.

This rings true to me. If I develop some package that consumes another package, and that package throws an exception, now _I_ also have to be aware of that, regardless of whether I want to use that feature. This is an uncommon facet among proposed language features; most are things that a programmer can opt into, or just not use at their discretion. Exceptions, by their very intention, cross all sorts of boundaries, expected or otherwise.

I said "almost entirely useless". As in, I only need one line of information from it but it's dozens of lines long.

And huge Go traces with hundreds of goroutines are somehow more useful? I don't understand where you're going with this. Java and Go are exactly the same here. And occasionally you do find it useful to observe full stack to understand how your code ended up where it crashed. C# and Go traces helped me multiple times with that.

Then you're not listening. Go back and read the part about implicit contracts.

I read it, nothing changed. In my experience it's not a problem. That's what documentation is for in both languages (net.ParseIP for example). If you forget to check if you value is nil/null or not you have the exact same problem in both languages. In most cases Go will return an error and C# will throw an exception so you don't even need to worry about nil. Good API doesn't just return you null without throwing an exception or something to tell what's wrong. In other cases you check for it explicitly. The most common types of errors with null in my experience are when you have protocol buffers where each field is a pointer/object or you have internal logic where class/struct fields could be nil depending on internal state and you forget to check for it before access. That's the most common pattern for me and nothing in Go significantly alleviates this problem. I can name two things that do help a bit - useful empty values and value types. But it's more about ease of programming because you're not required to construct every variable before use.

And that's exactly the problem -- errors getting propagated and control flow changing with nothing explicit to mark that it's going to happen. Apparently you don't think it's a problem, but others disagree.

That's a problem, I never said otherwise but people here are so fixated on Java/C#/C++ exceptions that they ignore anything that slightly resembles them. Exactly why Swift requires you to mark functions with throws so that you can see exactly what you should expect from a function and where control flow might break and in Rust you use ? to explicitly propagate an error with various helper methods to give it more context. They both use the same concept of errors as values but wrap it in syntactic sugar to reduce boilerplate.

And huge Go traces with hundreds of goroutines are somehow more useful?

With Go, you deal with errors by logging them along with the location at the point they are detected. There's no backtrace unless you choose to add one. I've only once needed to do that.

In my experience it's not a problem.

Well, my experience differs, and I think most people's experiences differ, and as evidence of that I cite the fact that Java 8 added Optional types.

This thread here discussed a lot of the strengths and weaknesses of Go and its error handling system, including a discussion about exceptions or not, I highly recommend reading it:

https://elixirforum.com/t/discussing-go-split-thread/13006/2

My 2 cents to error handling (sorry if such idea was mentioned above).

We want to rethrow errors in most cases. This leads to such snippets:

a, err := fn()
if err != nil {
    return err
}
use(a)
return nil

Let's rethrow non-nil error automatically if it was not assigned to a variable (without any extra syntax). The above code will become:

a := fn()
use(a)

// or just

use(fn())

Compiler will save err to implicit (invisible) variable, check it for nil and proceed (if err == nil) or return it (if err != nil) and return nil at the end of function if no errors occured during the function execution as usually but automatically and implicitly.

If err must be handled it have to be assigned to an explicit variable and used:

a, err := fn()
if err != nil {
    doSomething(err)
} else {
    use(a)
}
return nil

Error may be suppressed such a way:

a, _ := fn()
use(a)

In rare (fantastic) cases with more than one errors returned, explicit error handling will be mandatory (like now):

err1, err2 := fn2()
if err1 != nil || err2 != nil {
    return err1, err2
}
return nil, nil

That's my argument also - we want to rethrow errors in most cases, that's usually the default case. And maybe give it some context. With exceptions the context is added automatically by stack traces. With errors like in Go we do it by hand by adding error message. Why not make it more simple. And that's exactly what other languages are trying to do while balancing it with the issue of clarity.

So I agree with "Let's rethrow non-nil error automatically if it was not assigned to a variable (without any extra syntax)" but the latter part bugs me. That's where the root of the problem with exceptions and why, I think, people are so against talking about anything slightly related to them. They change control flow without any extra syntax. That's a bad thing.

If you look at Swift, for example, this code will not compile

func a() throws {}
func b() throws {
  a()
}

a can throw an error so you have to write try a() to even propagate an error. If you remove throws from b then it will not compile even with try a(). You have to handle the error inside b. That's a much better way of handling errors that solves both the issue of unclear control flow of exceptions and verbosity of Objective-C errors. The latter being pretty much exactly like errors in Go and what Swift is meant to replace. What I don't like is try, catch stuff that Swift also uses. I would much prefer to leave errors as part of a return value.

So what I would propose is to actually have the extra syntax. So that the call site tells by itself that it's a potential place where control flow might change. What I would also propose is that not writing this extra syntax would produce a compile error. That, unlike how Go works now, would force you to handle the error. You could throw in the ability to just silent the error with something like _ because in some cases it would be very frustrating to handle every little error. Like, printf. I don't care if it fails to log something. Go already has these annoying imports. But that solved with tooling at least.

There're two alternatives to compile time error I can think of right now. Like Go now, let the error be silently ignored. I don't like that and that was always my problem with Go error handling. It doesn't force anything, default behavior is to silently ignore the error. That's bad, that's not how you write robust and easy to debug programs. I had too many cases in Objective-C when I was lazy or out of time and ignored the error just to be hit with a bug in that same code but without any diagnostic info as to why it happened. At least logging it would allow me to solve the problem right there in many cases.

The downside is that people may start ignoring errors, place try, catch(...) everywhere so to speak. That's a possibility but, at the same time, with errors ignored by default it's even easier to do so. I think argument about exceptions doesn't apply here. With exceptions, what some people are trying to achieve is an illusion of their program being more stable. The fact that unhandled exception crashes the program is the problem here.

Other alternative would be to panic. But that's just frustrating and brings memory of exceptions. That definitely would lead people to do "defensive" coding so that their program doesn't crash. For me modern language should do as much stuff as possible at compile time and leave as few decisions to runtime as possible. Where panic might be appropriate is at top of the call stack. For example, not handling an error in the main function would automatically produce a panic. Does this also apply to goroutines? Probably shouldn't.

Why consider compromises?

@nick-korsakov the original proposal (this issue) wants to add more context to errors:

It is already easy (perhaps too easy) to ignore the error (see #20803). Many existing proposals for error handling make it easier to return the error unmodified (e.g., #16225, #18721, #21146, #21155). Few make it easier to return the error with additional information.

See also this comment.

In this comment I suggest that to make progress in this discussion (rather than running in loops) we should define better the goals e.g. what is a carefully handled error message. The whole thing is pretty interesting to read but it seems affected by a three-second goldfish memory problem (not much focused/moving forward, repeating nice creative syntax changes and arguments about exceptions/panics etc).

Another bikeshed:

func makeFile(url string) (size int, err error){
    rsp, err := http.Get(url)
    try err
    defer rsp.Body.Close()

    var data dataStruct
    dec := json.NewDecoder(rsp.Body)
    err := dec.Decode(&data)
    try errors.Errorf("could not decode %s: %v", url, err)

    f, err := os.Create(data.Path)
    try errors.Errorf("could not open file %s: %v", data.Path, err)
    defer f.Close()

    return f.Write([]byte(data.Rows))
}

try means "return if not the empty value". In this I'm assuming errors.Errorf will return nil when err is nil. I think this is about as much savings as we can expect while sticking with the goal of easy wrapping.

The scanner types in the standard library store the error state inside of a structure whose methods can responsibly check for the existence of an error before proceeding.

type Scanner struct{
    err error
}
func (s *Scanner) Scan() bool{
   if s.err != nil{
       return false
   }
   // scanning logic
}
func (s *Scanner) Err() error{ return s.err }

By using types to store the error state, it's possible to keep the code that uses such a type free of redundant error checks.

It also requires no creative and bizarre syntax changes or unexpected control transfers in the language.

I have to also suggest something like try/catch, where err is defined inside try{}, and if err is set to non-nil value - flow breaks from try{} to err handler blocks (if there are any).

Internally there are no exceptions, but the whole thing should be closer
to syntax that do if err != nil break checks after every line where err could be assigned.
Eg:

...
try(err) {
   err = doSomethig()
   err, value := doSomethingElse()
   doSomethingObliviousToErr()
   err = thirdErrorProneThing()
} 
catch(err SomeErrorType) {
   handleSomeSpecificErr(err)
}
catch(err Error) {
  panic(err)
}

I know it looks like C++, but it is also well known and cleaner than manual if err != nil {...} after every line.

@as

The scanner type works because it is doing all the work, therefore can afford to keep track of its own error along the way. Lets not kid ourselves that this is a universal solution, please.

@carlmjohnson

If we want one liner handling for simple error, we could change the syntax to allow the return statement to be the beggining of a one line block.
It would allow people to write:

func makeFile(url string) (size int, err error){
    rsp, err := http.Get(url)
    if err != nil return err
    defer rsp.Body.Close()

    var data dataStruct
    dec := json.NewDecoder(rsp.Body)
    err := dec.Decode(&data)
    if err != nil return errors.Errorf("could not decode %s: %v", url, err)

    f, err := os.Create(data.Path)
    if err != nil return errors.Errorf("could not open file %s: %v", data.Path, err)
    defer f.Close()

    return f.Write([]byte(data.Rows))
}

I think the spec should be changed to something like (this could be quite naive :))

Block = "{" StatementList "}" | "return" Expression .

I don't think that special casing return is really better than just changing gofmt to make simple if err checks one line instead of three.

@urandom

Error coalescing beyond one boxable type and its actions shouldn't be encouraged. To me it indicates a lack of effort to wrap or add error context between errors originating from different unrelated actions.

Scanner approach is one of the worst things that I read in context of this whole "errors are values" mantra:

  1. It's useless in pretty much every use-case that requires lots of error handling boiler-plate. Functions that call multiple external packages will not benefit from it.
  2. It's convoluted and unfamiliar concept. Introducing it will only confuse future readers and make your code more complicated than it needs to be just so you can work around language design deficiency.
  3. It hides logic and tries to be similar to exceptions by taking the worst from it (complex control flow) without taking any benefits.
  4. In some cases it will waste compute resources. Every call will have to waste time on useless error check that happened ages ago.
  5. It hides the exact place where error happened. Imagine a case where you parse or serialize some file format. You would have a chain of read/write calls. Imagine the first one fails. How would you tell, where exactly the error happened? Which field it was parsing or serializing? "IO error", "timeout" - these errors would be useless in this case. You could provide context to each read/write (field name, for example). But at this point it's better to just give up on the whole approach as it's working against you.

In some cases it will waste compute resources.

Benchmarks? What is a "compute resource" exactly?

It hides the exact place where error happened.

No, it doesn't, because non-nil errors aren't overwritten

Functions that call multiple external packages will not benefit from it.
It's convoluted and unfamiliar concept
Scanner approach is one of the worst things that I read in context of this whole "errors are values"

My impression is you don't understand the approach. It is logically equivalent to regular error check in a self-contained type, I suggest you study the example closely so maybe it can be the worst thing you understand rather than just the worst thing you've _read_.

Sorry, I'm going to add my own proposal to the pile. I've read through most of what is here, and while I do like some of the proposals, I feel they're trying to do too much. The issue is error boilerplate. My proposal is simply to eliminate that boilerplate at the syntax level, and leave the ways errors are passed around alone.

Proposal

Reduce error boilerplate by enabling the use of the _! token as syntactic sugar to cause a panic when assigned a non-nil error value

val, err := something.MayError()
if err != nil {
    panic(err)
}

could become

val, _! := something.MayError()

and

if err := something.MayError(); err != nil {
    panic(err)
}

could become

_! = something.MayError()

Of course, the particular symbol is up for debate. I also considered _^, _*, @, and others. I chose _! as the de-facto suggestion because I thought it would be the most familiar at a glance.

Syntactically, _! (or the chosen token) would be a symbol of type error available in the scope in which it is used. It starts out as nil, and any time it is assigned to, a nil check is performed. If it is set to a non-nil error value, a panic is started. Because _! (or, again, the token chosen) would not a syntactically valid identifier in go, name collision wouldn't be a concern. This ethereal variable would only be introduced in scopes where it is used, similar to named return values. If a syntactically valid identifier is needed, perhaps a placeholder could be used that would be re-written to a unique name at compile time.

Justification

One of the more common criticisms I see leveled at go is the verbosity of error handling. Errors at API boundaries aren't a bad thing. Having to get the errors to the API boundaries can be a pain though, especially for deeply recursive algorithms. To get around the added verbosity error propagation introduces to recursive code, panics can be used. I feel that this is a pretty commonly used technique. I've used it in my own code, and I've seen it used in the wild, including in go's parser. Sometimes, you've done validation elsewhere in your program and are expecting an error to be nil. If a non-nil error were to be received, this would violate your invariant. When an invariant is violated, it's acceptable to panic. In complex initialization code, sometimes it makes sense to turn errors into panics and recover them to be returned somewhere with more knowledge of the context. In all of these scenarios, there's an opportunity to reduce error boilerplate.

I realize that it is go's philosophy to avoid panics as much as possible. They are not a tool for error propagation across API boundaries. However, they are a feature of the language and have legitimate use cases, such as those described above. Panics are a fantastic way to simplify error propagation in private code, and a simplification of the syntax would go a long way to make code cleaner and, arguably, clearer. I feel that it is easier to recognize _! (or @, or `_^, etc...) at a glance than the "if-error-panic" form. A token can dramatically decrease the amount of code that must be written/read to convey/understand:

  1. there could be an error
  2. if there is an error, we're not expecting it
  3. if there is an error, it's probably being handled up the chain

As with any syntax feature, there's the potential for abuse. In this case, the go community already has a set of best practices for dealing with panics. Since this syntax addition is syntactic sugar for panic, that set of best practices can be applied to its use.

In addition to simplification of the acceptable use cases for panic, this also makes fast prototyping in go easier. If I have an idea I want to jot down in code, and just want errors to crash the program while I toy around, I could make use of this syntax addition rather than the "if-error-panic" form. If I can express myself in less lines in the early stages of development allows me to get my ideas into code faster. Once I have a complete idea in code, I go back and refactor my code to return errors at appropriate boundaries. I wouldn't leave free panics in production code, but they can be a powerful development tool.

Panics are just exceptions by another name, and one thing I love about Go is that exceptions are exceptional. I don't want to encourage more exceptions by giving them syntactic sugar.

@carlmjohnson One of two things has to be true:

  1. Panics are a part of the language with legitimate use cases, or
  2. Panics do not have legitimate use cases, and therefore should be removed from the language

I suspect the answer is 1.
I also disagree that "panics are just exceptions by another name". I think that kind of hand-waving prevents real discussion. There are key differences between panics and exceptions as seen in most other languages.

I understand the knee-jerk "panics are bad" reaction, but personal feelings about use of panic does not change the fact that panics are used, and are in fact useful. The go compiler uses panics to bail out of deeply recursive processes in both the parser and in the type-checking phase (last I looked).
Using them to propagate errors through deeply recursive code seems not only to be an acceptable use, but a use endorsed by the go developers.

Panics communicate something specific:

something went wrong here that here wasn't prepared to handle

There are always going to be places in code where that is true. Especially early on in development. Go's been modified to improve the refactoring experience before: the addition of type aliases. Being able to propagate unwanted errors with panic until you can flesh out if and how to handle them at a level closer to the source can make writing and progressively refactoring code much less verbose.

I feel like most proposals here are proposing large changes to the language. This is the most transparent approach I could come up with. It allows the entire current cognitive model of error handling in go to remain intact, while enabling syntax reduction for a specific, but common, case. Best practices currently dictate that "go code shouldn't panic across API boundaries". If I have public methods in a package, they should return errors if something goes wrong, except on rare occasions where the error is unrecoverable (invariant violations, for example). This addition to the language does would not supersede that best practice. This is simply a way to reduce boilerplate in internal code, and make sketching ideas more clear. It certainly makes code easier to read linearly.

var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

is a lot more readable than

var1, err := trySomeTask1()
if err != nil {
    panic(err)
}
var2, err := trySomeTask2(var1)
if err != nil {
    panic(err)
}
var3, err := trySomeTask3(var2)
if err != nil {
    panic(err)
}
var4, err := trySomeTask4(var3)
if err != nil {
    panic(err)
}

There really is no fundamental difference between a panic in Go and an exception in Java or Python etc. apart from syntax and lack of an object hierarchy (which makes sense because Go doesn't have inheritance). How they work and how they are used is the same.

Of course panics have a legitimate place in the language. Panics are for handling errors that should only occur to due to programmer error that are otherwise unrecoverable. For example, if you divide by zero in an integer context, there is no possible return value and it's your own fault for not checking for zero first, so it panics. Similarly, if you read a slice out of bounds, try use nil as a value, etc. Those things are caused by programmer error—not by an anticipated condition, like the network being down or a file having bad permissions—so they just panic and blow up the stack. Go provides some helper functions that panic like template.Must because it's anticipated that those will be used with hardcoded strings where any error would have to be caused by programmer error. Out of memory is not a programmer fault per se, but it's also unrecoverable and can happen anywhere, so it's not an error but a panic.

People also sometimes use panics as a way of shortcircuiting the stack, but that's generally frowned on for readability and performance reasons, and I don't see any chance of Go changing to encourage its use.

Go panics and Java's unchecked exceptions are pretty much identical and they exist for the same reasons and to handle the same usecases. Don't encourage people to use panics for other cases because those cases have the same problems as exceptions in other languages.

People also sometimes use panics as a way of shortcircuiting the stack, but that's generally frowned on for readability and performance reasons

First of all, the readability issue is something this syntax change directly addresses:

// clearly, linearly shows that these steps must occur in order,
// and any errors returned cause a panic, because this piece of
// code isn't responsible for reporting or handling possible failures:
// - IO Error: either network or disk read/write failed
// - External service error: some unexpected response from the external service
// - etc...
// It's not this code's responsibility to be aware of or handle those scenarios.
// That's perhaps the parent process's job.
var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

vs

var1, err := trySomeTask1()
if err != nil {
    panic(err)
}
var2, err := trySomeTask2(var1)
if err != nil {
    panic(err)
}
var3, err := trySomeTask3(var2)
if err != nil {
    panic(err)
}
var4, err := trySomeTask4(var3)
if err != nil {
    panic(err)
}

Leaving readability aside for the moment, the other reason given is performance.
Yes, it's true that using panics and defer statements incurs a performance penalty, but in many cases this difference is negligible to the operation being performed. Disk & network IO are going to, on average, take much longer than any potential stack magic for managing defers/panics.

I hear this point parroted a lot when discussing panics, and I think it's disingenuous to say that panics are a performance degradation. They certainly CAN be, but they don't have to be. Just like many other things in a language. If you're panicking inside a tight loop where the performance hit would really matter, you should not also be deferring within that loop. In fact, any function that itself chooses to panic should generally not catch it's own panic. Similarly, a go function written today wouldn't both return an error and panic. That's unclear, silly, and not best practice. Perhaps that's how we may be used to seeing exceptions used in Java, Python, Javascript, etc, but that's not how panics are generally used in go code, and I don't believe that adding an operator specifically for the case of propagating an error up the call stack via panic is going to change the way people use panic. They're using panic anyway. The point of this syntax extension is to acknowledge the fact that developers use panic, and it has uses that are perfectly legitimate, and reduce the boilerplate around it.

Can you give me some examples of problematic code that you think this syntax feature would enable, that are not currently possible/against best practices? If someone is communicating errors to the users of their code via panic/recover, that is currently frowned upon and would obviously still continue to be, even if syntax such as this were added. If you could, please answer the following:

  1. What abuses do you imagine would rise from a syntax extension like this?
  2. What does var1, err := trySomeTask1(); if err != nil { panic(err) } convey that var1, _! := trySomeTask1() does not? Why?

It seems to me that the crux of your argument is that "panics are bad and we shouldn't use them".
I can't unpack and discuss the reasons behind that if they're not shared.

Those things are caused by programmer error—not by an anticipated condition, like the network being down or a file having bad permissions—so they just panic and blow up the stack.

I, like most gophers, like the idea of errors-as-values. I believe it helps clearly communicate what parts of an API guarantee a result versus which can fail, without having to look at the documentation.

It allows for things like collecting errors and augmenting errors with more information. That is all very important at API boundaries, where your code intersects with user code. However, within those API boundaries it's often not necessary to do all of that with your errors. Especially if you're expecting the happy path, and have other code responsible for handling the error if that path fails.

There are some times when it's not your code's job to handle an error.
If I'm writing a library, I don't care if the network stack is down - that's outside of my control as a library developer. I'll return those errors back to the user code.

Even in my own code, there are times where I write a piece of code who's only job is to give the errors back to a parent function.

For example, say you have an http.HandlerFunc read a file from disk as the response - this will almost always work, and if it fails it's likely either the program is not properly written (programmer error) or there's a problem with the file system outside of the program's scope of responsibility. Once the http.HandlerFunc panics, it exits and some base handler will catch that panic and write a 500 to the client. If at any point in the future I'd like to handle that error differently, I can replace _! with err and do whatever I want with the error value. The thing is, for the life of the program, I will likely not need to do that. If I'm running into issues like that, the handler's not the part of the code responsible for handling that error.

I can, and normally do, write if err != nil { panic(err) } or if err != nil { return ..., err }in my handlers for things like IO failures, network failures, etc. When I do need to check an error, I can still do that. Most of the time, though, I'm just writing if err != nil { panic(err) }.

Or, another example, If I'm recursively searching a trie (say in an http router implementation), I'll declare a function func (root *Node) Find(path string) (found Value, err error). That function will defer a function to recover any panics generated going down the tree. What if the program is creating malformed tries? What if some IO fails because the program isn't running as a user with the correct permissions? These issues aren't the problem of my trie search algorithm - unless I explicitly make it so later - but they're possible errors I may encounter. Returning them all the way up the stack leads to a lot of extra verbosity, including holding what will ideally be several nil error values on the stack. Instead, I can choose to panic an error up to that public API function and return it to the user. At the moment, this still incurs that extra verbosity, but it doesn't have to.

Other proposals are discussing how to treat one return value as special. It's essentially the same thought, but instead of using features already built into the language, they look to modify the language's behavior for certain cases. In terms of ease of implementation, this type of proposal (syntactic sugar for something already supported) is going to be the easiest.

Edit to add:
I'm not married to the proposal that I've made as it is written, but I do think that looking at the error handling problem from a new angle is important. Nobody is suggesting anything that novel, and I want to see if we can reframe our understanding of the issue. The issue is that there are too many places where errors are being explicitly handled when they don't need to be, and developers would like a way to propagate them up the stack without extra boilerplate code. It turns out Go already has that feature, but there's not nice syntax for it. This is a discussion of wrapping existing functionality in a less verbose syntax to make the langauge more ergonimic without changing the behavior. Isn't that a win, if we can accomplish it?

@mccolljr Thanks, but one of the goals of this proposal is to encourage people to develop new ways to handle all three cases of error handling: ignore the error, return the error unmodified, return the error with additional contextual information. Your panic proposal does not address the third case. It's an important one.

@mccolljr I think the API boundaries are a lot more common than you seem to assume. I don't see within-API calls as the common case. If anything, it might be the other way around (some data would be interesting here). So I'm not sure developing special syntax for within-API calls is the right direction. In addition, using returned errors, rather than paniced errors, within an API is typically a fine way to go (especially if we come up with a plan for this issue). paniced errors have their uses, but situations where they are demonstrably better seem rare.

I don't believe that adding an operator specifically for the case of propagating an error up the call stack via panic is going to change the way people use panic.

I think you're mistaken. People will reach for your shorthand operator because it's so convenient, and then they'll end up using panic a lot more than before.

Whether panics are useful sometimes or rarely, and whether they are useful across or within API boundaries, are red herrings. There are a lot of actions one might take on an error. We're looking for a way to shorten error-handling code without privileging one action over the others.

but in many cases this difference is negligible to the operation being performed

While true, I think it's a dangerous road to take. Looking negligible at first it would pile up and eventually cause bottlenecks later down the road when it's already late. I think we should keep performance in mind from the beginning and try to find better solutions. Already mentioned Swift and Rust do have error propagation but implement it as, basically, simple returns wrapped into syntactic sugar. Yes, it's easy to reuse existing solution but I would prefer to leave everything as is than to simplify and encourage people to use panics hidden behind unfamiliar syntactic sugar that tries to hide the fact that it's, basically, exceptions.

Looking negligible at first it would pile up and eventually cause bottlenecks later down the road when it's already late.

No thanks. Imaginary performance bottlenecks are geometrically negligible performance bottlenecks.

No thanks. Imaginary performance bottlenecks are geometrically negligible performance bottlenecks.

Please leave your personal feelings out of this topic. You obviously have some problem with me and don't want to bring anything useful, so just ignore my comments and leave a down vote as you did with pretty much every comment before. No need to keep posting these nonsensical answers.

I don't have a problem with you, you are just making claims about performance bottlenecks without any data to back that up and I'm pointing that out with words and thumbs.

Folks, please keep the conversation respectful and on-topic. This issue is about handling Go errors.

https://golang.org/conduct

I'd like to revisit the "return the error/with additional context" part again, since I assume ignoring the error is already covered by the already existing _.

I'm proposing a two-word keyword that may be followed by an string (optionally). The reason it is a two-word keyword is twofold. First, unlike an operator which is inherently cryptic, it's easier to grasp what it does without too much prior knowledge. I've picked "or bubble", because I'm hoping that the word or with an absence of an assigned error will signify to the user that the error is being handled here, if it is not nil. Some users will already associate the or with handling a falsy value from other languages (perl, python), and reading data := Foo() or ... might subconsciously tell them that data is unusable if the or part of the statement is reached. Second, the bubble keyword while being relatively short, might signify to the user that something is going up (the stack). The up word might also be suitable, though I'm not sure whether the whole or up is understandable enough. Finally, the whole thing is a keyword, first and foremost because its more readable, and second because that behavior cannot be written by a function itself (you might be able to call panic to escape the function you are in, but then you can't stop yourself, someone else will have to recover).

The following is only for error propagation, therefore may only be used in functions that return an error, and the zero values of any other return arguments:

For returning an error without modifying it in any way:

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble

    return data;
}

For returning an error with an additional message:

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble fmt.Sprintf("reading file %s", path)

    modified := modifyData(data) or bubble "modifying the data"

    return data;
}

And finally, introduce a global adaptor mechanism for customized error modification:

// Default Bubble Processor
errors.BubbleProcessor(func(msg string, err error) error {
    return fmt.Errorf("%s: %v", msg, err)
})

// Some program might register the following:
errors.BubbleProcessor(func(msg string, err error) error {
    return errors.WithMessage(err, msg)
})

Finally, for the few places where really complex handling is necessary, the already existing verbose way is already the best way.

Interesting. Having a global bubble handler gives people who want stack traces a place to put the call for a trace, which is a nice benefit of that method. OTOH, if it has the signature func(string, error) error, then that means bubbling has to be done with the built-in error type and not any other type, such as a concrete type implementing error.

Also, the existence of or bubble suggests the possibility of or die or or panic. I'm not sure if that's a feature or a bug.

unlike an operator which is inherently cryptic, it's easier to grasp what it does without too much prior knowledge

That's might be good when you first encounter it. But reading and writing it again and again - it seems too verbose and takes too much space to convey a pretty simple thing - unhandled error with bubble up the stack. Operators are cryptic at first but they're concise and have a good contrast with all the other code. They clearly separate main logic from error handling because it is in fact a separator. Having so much words in one line will hurt readability in my opinion. At least merge them into orbubble or drop one of them. I don't see a point of having two keywords there. It turns Go into a spoken language and we know how that goes (VB, for example)

I'm not much of a fan of a global adaptor. If my package sets a custom processor, and yours also sets a custom processor, whose wins?

@object88
I'm thinking it is similar to the default logger. You only set the output once (in your program), and that affects all packages that you use.

Error handling is very different from logging; one describes the program's informative output, the other manages the program's flow. If I set up the adaptor to do one thing in my package, that I need to properly manage the logical flow, and another package or program alters that, you are in a Bad Place.

Please bring back the Try Catch Finally and we don't need anymore fights. It makes everyone happy. There is nothing wrong with borrowing features and syntaxes from other programming languages. Java did it and C# did that as well and they both are really successful programming languages. GO community (or authors) please be open to changes when it is needed.

@KamyarM , I respectfully disagree; try/catch does _not_ make everyone happy. Even if you wanted to implement that in your code, a thrown exception means that everyone who uses your code needs to handle exceptions. That is not a language change that can be localized to your code.

@object88
Actually, it seems to me that a bubble processor describes the program's informative error output, making it not that different from a logger. And I imagine you want a single error representation throughout your application, and to not vary from package to package.

Though perhaps you can provide a short example, maybe there's something I'm missing.

Thank you so much for your thumbs down. That's exactly the issue I am talking about. GO community is not open to changes and I feel it and I really don't like that.

This is probably not related to this case but I was looking another day for Go equivalent of C++'s ternary operator and I came across to this alternative approach:

v := map[bool]int{true: first_expression, false: second_expression} [condition]
instead of simply
v= condition ? first_expression : second_expression;

Which one of the 2 forms you guys prefer? An unreadable code above (Go My Way) with probably lots of performance issues or the second simple syntax in C++ (Highway)? I prefer the highway guys. I don't know about you.

So to summarize please bring new syntaxes , borrow them from other programming languages. There is nothing wrong with that.

Best Regards,

GO community is not open to changes and I feel it and I really don't like that.

I think this mischaracterizes the attitude underlying what you are experiencing. Yes, the community produces a lot of push back when someone proposes try/catch or ?:. But the reason is not that we're resistant to new ideas. We almost all have experience using languages with these features. We are quite familiar with them, and someone of us have used them on a daily basis for years. Our resistance is based on the fact that these are _old ideas_, not new ones. We already embraced a change: a change away from try/catch and a change away from using ?:. What we are resistant to is changing _back_ to using these things we already used and didn't like.

Actually, it seems to me that a bubble processor describes the program's informative error output, making it not that different from a logger. And I imagine you want a single error representation throughout your application, and to not vary from package to package.

What if someone wanted to using bubbling to pass stack traces and then use that to make a decision. For example, if the error originates from a file operation, then fail but if it originates from the network, wait and try again. I could see building some logic for this into an error handler, but if there is only one error handler per runtime, that would be a recipe for conflict.

@urandom , maybe this is a trivial example, but let's say that my adapter returns another struct that implements error, which I expect to consume elsewhere in my code. If another adapter comes along and replaces my adapter, then my code stops working correctly.

@KamyarM The language and its idioms go together. When we consider changes to error-handling, we're not just talking about changing the syntax, but (potentially) also the very structure of the code.

Try-catch-finally would be a very invasive such change: it would fundamentally change the way that Go programs are structured. In contrast, most of the other proposals you see here are local to each function: errors are still values returned explicitly, control flow avoids non-local jumps, etc.

To use your example of a ternary operator: yes, you can fake one today using a map, but I hope you won't actually find that in production code. It doesn't follow the idioms. Instead, you'll usually see something more like:

    var v int
    if condition {
        v = first_expression
    } else {
        v = second_expression
    }

It's not that we don't want to borrow syntax, it's that we have to consider how it would fit in with the rest of the language and the rest of the code that already exists today.

@KamyarM I use both Go and Java, and I emphatically _do not_ want Go to copy exception handling from Java. If you want Java, use Java. And please take discussion of ternary operators to an appropriate issue, e.g. #23248.

@lpar So If I work for a company and for some unknown reason they picked GoLang as their programming language, I simply need to quit my job and apply for a Java one!? Come on man!

@bcmills You can count the code you suggested there. I think that's 6 lines of code instead of one and probably you get couple of code cyclomatic complexity points for that (You guys use Linter. right?).

@carlmjohnson and @bcmills Any syntax that is old and mature doesn't mean that is bad. Actually I think the if else syntax is way older than ternary operator syntax.

Good that you brought this GO idiom thing. I think that is just one of the issue of this language. Whenever there is request for a changes someone says oh no that's against Go idiom. I see it as just an excuse to resist changes and block any new ideas.

@KamyarM please be polite. If you'd like to read more about some of thinking behind keeping the language small, I recommend https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html.

Also, a general comment, unrelated to the recent discussion of try/catch.

There have been many proposals in this thread. Speaking for myself, I still do not feel that I have a strong grasp on the problem(s) to be solved. I would love to hear more about them.

I'd also be excited if someone wanted to take on the unenviable but important task of maintaining an organized and summarized list of problems that have been discussed.

@josharian I was just talking frankly there. I wanted to show the exact issues in the language or community. Consider that as more criticism. GoLang is open to criticism, is that right?

@KamyarM If you worked for a company that had picked Rust for its programming language, would you go to the Rust Github and start demanding garbage-collected memory management and C++-style pointers so you don't have to deal with the borrow checker?

The reason Go programmers don't want Java-style exceptions has nothing to do with lack of familiarity with them. I first encountered exceptions in 1988 via Lisp, and I'm sure there are other people in this thread who encountered them even earlier -- the idea goes back to the early 1970s.

The same is even more true of ternary expressions. Read up on Go's history -- Ken Thompson, one of Go's creators, implemented the ternary operator in the B language (predecessor of C) at Bell Labs in 1969. I think it's safe to say he was aware of its benefits and pitfalls when considering whether to include it in Go.

Go is open to criticism but we require that discussion in the Go forums be polite. Being frank is not the same as being impolite. See the "Gopher Values" section of https://golang.org/conduct. Thanks.

@lpar Yes, If Rust has a such a forum I would do that ;-) Seriously I would do that. Because I want my voice to be heard.

@ianlancetaylor Did I use any vulgar words or language? Did I use discrimination language or any bullying of someone or any unwelcome sexual advances? I don't think so.
Come on man, We are just talking about Go programming language here. It is not about a religion or politics or anything like that.
I was frank. I wanted my voice to be heard. I think that's why there is this forum. For voices to be heard. You may not like my suggestion or my criticism. That's OK. But I guess you need to let me talk and discuss otherwise we can all conclude that everything is perfect and there is no problem and so no needs for further discussions.

@josharian Thank you for the article I will take a look at that.

Well, I looked backed at my comments to see if there is anything bad there. The only thing I might have insulted(I still call that criticism btw) is GoLang programming language Idioms! Hahah!

To go back to our topic, If you hear my voice, please Go Authors consider bringing back the Try catch blocks. Leave it to the programmer to decide to use it in the right place or not (you already have something similar, I mean the panic defer recover then why not Try Catch which is more familiar to programmers?).
I suggested a workaround for the current Go Error handling for backward compatibility. I am not saying that's the best option but I think it is viable.

I will retire myself from discussing more on this topic.

Thank you for the opportunity.

@KamyarM You are confusing our requests that you remain polite with our disagreement with your arguments. When people disagree with you, you are responding in personal terms with comments like "Thank you so much for your thumbs down. That's exactly the issue I am talking about. GO community is not open to changes and I feel it and I really don't like that."

Once again: please be polite. Stick to technical arguments. Avoid ad hominem arguments that attack people rather than ideas. If you genuine don't understand what I mean, I am willing to discuss it offline; e-mail me. Thanks.

I'll throw my 2c in, and hope it's not literally repeating something in the other N hundred comments (or stepping on the discussion of urandom's proposal).

I like the original idea that was posted, but with two primary tweaks:

  • Syntactic bikeshedding: I strongly believe that anything that has implicit control flow should be an operator on its own, rather than an overload of an existing operator. I'll throw ?! out there, but I'm happy with whatever isn't easily confused with an existing operator in Go.

  • The RHS of this operator should take a function, rather than an expression with an arbitrarily injected value. This would let devs write pretty terse error-handling code, while still being clear about their intent, and flexible with what they can do, e.g.

func returnErrorf(s string, args ...interface{}) func(error) error {
  return func(err error) error {
    return errors.New(fmt.Sprintf(s, args...) + ": " + err.Error())
  }
}

func foo(r io.ReadCloser, callAfterClosing func() error, bs []byte) ([]byte, error) {
  // If r.Read fails, returns `nil, errors.New("reading from r: " + err.Error())`
  n := r.Read(bs) ?! returnErrorf("reading from r")
  bs = bs[:n]
  // If r.Close() fails, returns `nil, errors.New("closing r after reading [[bs's contents]]: " + err.Error())`
  r.Close() ?! returnErrorf("closing r after reading %q", string(bs))
  // Not that I advocate this inline-func approach, but...
  callAfterClosing() ?! func(err error) error { return errors.New("oh no!") }
  return bs, nil
}

The RHS should never be evaluated if an error doesn't happen, so this code won't allocate any closures or whatever on the happy path.

It's also pretty straightforward to "overload" this pattern to work in more interesting cases. I have three examples in mind.

First, we could have the return be conditional if the RHS is a func(error) (error, bool), like so (if we allow this, I think we should use a distinct operator from the unconditional returns. I'll use ??, but my "I don't care as long as it's distinct" statement still applies):

func maybeReturnError(err error) (error, bool) {
  if err == io.EOF {
    return nil, false
  }
  return err, true
}

func id(err error) error { return err }

func ignoreError(err error) (error, bool) { return nil, false }

func foo(n int) error {
  // Does nothing
  id(io.EOF) ?? ignoreError
  // Still does nothing
  id(io.EOF) ?? maybeReturnError
  // Returns the given error
  id(errors.New("oh no")) ?? maybeReturnError
  return nil
}

Alternatively, we could accept RHS functions that have return types matching that of the outer function, like so:

func foo(r io.Reader) ([]int, error) {
  returnError := func(err error) ([]int, error) { return []int{0}, err }
  // returns `[]int{0}, err` on a Read failure
  n := r.Read(make([]byte, 4)) ?! returnError
  return []int{n}, nil
}

And finally, if we really want, we can generalize this to work with more than just errors by changing the argument type:

func returnOpFailed(name string) func(bool) error {
  return func(_ bool) error {
    return errors.New(name + " failed")
  }
}

func returnErrOpFailed(name string) func(error) error {
  return func(err error) error {
    return errors.New(name + " failed: " + err.Error())
  }
}

func foo(c chan int, readInt func() (int, error), d map[int]string) (string, error) {
  n := <-c ?! returnOpFailed("receiving from channel")
  m := readInt() ?! returnErrOpFailed("reading an int")
  result := d[n + m] ?! returnOpFailed("looking up the number")
  return result, nil
}

...Which I would personally find really useful when I have to do something terrible, like hand-decoding a map[string]interface{}.

To be clear, I'm primarily showing the extensions as examples. I'm not sure which of them (if any) strike a good balance between simplicity, clarity, and general usefulness.

I'd like to revisit the "return the error/with additional context" part again, since I assume ignoring the error is already covered by the already existing _.

I'm proposing a two-word keyword that may be followed by an string (optionally).

@urandom the first part of your proposal is agreeable, one could always start with that and leave the BubbleProcessor for a second revision. The concerns raised by @object88 are valid IMO; I have recently seen advices like "you should not overwrite http's default client/transport", this would become another of those.

There have been many proposals in this thread. Speaking for myself, I still do not feel that I have a strong grasp on the problem(s) to be solved. I would love to hear more about them.

I'd also be excited if someone wanted to take on the unenviable but important task of maintaining an organized and summarized list of problems that have been discussed.

It could be you @josharian if @ianlancetaylor appoints you? :blush: I don't know how other issues are being planned/discussed but perhaps this discussion is just being used as a "suggestions box"?

@KamyarM

@bcmills You can count the code you suggested there. I think that's 6 lines of code instead of one and probably you get couple of code cyclomatic complexity points for that (You guys use Linter. right?).

Hiding cyclomatic complexity makes it harder to see but it doesn't remove it (remember strlen?). Just as making error handling "shortened" makes the error handling semantics easier to ignore--but harder to see.

Any statements or expressions in the source that reroutes flow control should be obvious and terse, but if it's a decision between obvious or terse, the obvious should be preferred in this case.

Good that you brought this GO idiom thing. I think that is just one of the issue of this language. Whenever there is request for a changes someone says oh no that's against Go idiom. I see it as just an excuse to resist changes and block any new ideas.

There is a difference between new and beneficial. Do you believe that because you have an idea, the very existence of it merits approval? As an exercise, please look at the issue tracker and try to imagine Go today if every single idea was approved regardless of what the community thought.

Perhaps you believe that your idea is better than the others. That's where the discussion comes in. Instead of degenerating the conversation to talking about how the entire system is broken because of idioms, address criticisms directly, point-by-point, or find a middle ground between you and your peers.

@gdm85
I added the processor for some sort of customization on the part of the returned error. And while I do believe that it is a bit like using the default logger, in that you can get away with using it throughout most of the time, I did say that I'm open to suggestions. And for the record, I don't believe that the default logger and default http client are even remotely in the same category.

I also like @gburgessiv 's proposal, although I'm not a big fan of the cryptic operator itself (maybe at least pick ? like in Rust, though I still think that's cryptic). Would this look more readable:

func foo(r io.ReadCloser, callAfterClosing func() error, bs []byte) ([]byte, error) {
  // If r.Read fails, returns `nil, errors.New("reading from r: " + err.Error())`
  n := r.Read(bs) or returnErrorf("reading from r")
  bs = bs[:n]
  // If r.Close() fails, returns `nil, errors.New("closing r after reading [[bs's contents]]: " + err.Error())`
  r.Close() or returnErrorf("closing r after reading %q", string(bs))
  // Not that I advocate this inline-func approach, but...
  callAfterClosing() or func(err error) error { return errors.New("oh no!") }
  return bs, nil
}

And hopefully his proposal will also include a default implementation of a function similar to his returnErrorf somewhere in the errors package. Maybe errors.Returnf().

@KamyarM
You already expressed your opinion here, and didn't get any comment or reaction sympathetic to the exception cause. I don't see what repeating the same thing will accomplish, tbh, besides disrupting the other discussions. And if that is your goal, it's just not cool.

@josharian, I'll try to summarize the discussion briefly. It will be biased, since I have a proposal in the mix, and incomplete, since I'm not up to rereading the entire thread.

The problem we're trying to address is the visual clutter caused by Go error handling. Here's a good example (source):

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {
    if err := createFolderIfNotExist(to); err != nil {
        return nil, err
    }
    if err := clearFolder(to); err != nil {
        return nil, err
    }
    if err := cloneRepo(to, from); err != nil {
        return nil, err
    }
    dirs, err := getContentFolders(to)
    if err != nil {
        return nil, err
    }
    return dirs, nil
}

Several commenters in this thread don't think this needs fixing; they are happy that error-handling is intrusive, because dealing with errors is just as important as handling the non-error case. For them, none of the proposals here are worth it.

Proposals that do try to simplify code like this divide into a few groups.

Some propose a form of exception handling. Given that Go could have chosen exception handling at the beginning and chose not to, these seem unlikely to be accepted.

Many of the proposals here choose a default action, like returning from the function (the original proposal) or panicking, and suggest a bit of syntax that makes that action easy to express. In my opinion, all those proposals fail, because they privilege one action at the expense of others. I regularly use returns, t.Fatal and log.Fatal to handle errors, sometimes all in the same day.

Other proposals provide no way to augment or wrap the original error, or make it significantly harder to wrap than not. These also are inadequate, since wrapping is the only way to add context to errors, and if we make it too easy to skip, it will be done even less frequently than it is now.

Most of the remaining proposals add some sugar and sometimes a bit of magic to simplify things without constraining the possible actions or the ability to wrap. My and @bcmills's proposals add a minimal amount of sugar and zero magic to slightly increase readability, and also to prevent a nasty sort of bug.

A few other proposals add some sort of constrained non-local control flow, like an error-handling section at the beginning or end of a function.

Last but not least, @mpvl recognizes that error-handling can get very tricky in the presence of panics. He suggests a more radical change to Go error-handling to improve correctness as well as readability. He has a compelling argument, but in the end I think his cases don't require drastic changes and can be handled with existing mechanisms.

Apologies to anyone whose ideas aren't represented here.

I have a feeling someone's going to ask me about the difference between sugar and magic. (I'm asking that myself.)

Sugar is a bit of syntax that shortens code without fundamentally altering the rules of the language. The short assignment operator := is sugar. So is C's ternary operator ?:.

Magic is a more violent disruption to the language, like introducing a variable into a scope without declaring it, or performing a non-local control transfer.

The line is definitely blurry.

Thanks for doing that, @jba. Very helpful. Just to pull out the highlights, the problems identified so far are:

the visual clutter caused by Go error handling

and

error-handling can get very tricky in the presence of panics

If there are any other fundamentally different problems (not solutions) that @jba and I have missed, please do chime in (anyone). FWIW, I would consider ergonomics, code clutter, cyclomatic complexity, stutter, boilerplate, etc as variants of the "visual clutter" problem (or cluster of problems).

@josharian Do you want to consider the scoping problems (https://github.com/golang/go/issues/21161#issuecomment-319277657) as a variant of the “visual clutter” problem, or a separate issue?

@bcmills seems distinct to me, since it is about subtle correctness issues, as opposed to aesthetics/ergonomics (or at most correctness issues involving code bulk). Thanks! Want to edit my comment and add in a one-line synopsis of it?

I have a feeling someone's going to ask me about the difference between sugar and magic. (I'm asking that myself.)

I use this definition of magic: Looking at a bit of source code, if you can figure out what it is supposed to do by some variant of the following algorithm:

  1. Look up all the identifiers, keywords, and grammar constructs present on the line or in the function.
  2. For the grammar constructs and keywords, consult the official language documentation.
  3. For identifiers, there should be a clear mechanism for locating them using the information in the code you are looking at, using the scopes the code is currently located in, as defined by the language, from which you can get the identifier's definition, which will be accurate at run-time.

If this algorithm _reliably_ produces correct understandings of what the code is going to do, it is not magical. If it does not, then there is some amount of magic in it. How recursively you have to apply it as you try to follow documentation references and identifier definitions down to other identifier definitions affects the _complexity_, but not the _magicness_, of the constructs/code in question.

Examples of magic include: Identifiers with no clear path back to their origin because you imported them without a namespace (dot imports in Go, especially if you have more than one). Any ability a language may have to non-locally define what some operator will resolve to, such as in dynamic languages where code can non-locally completely redefine a function reference, or redefine what the language does for non-existent identifiers. Objects constructed by schemas loaded from a database at runtime, so at code time one is more-or-less blindly hoping they will be there.

The nice thing about this is that it gets almost all of the subjectivity out of the question.

Back on the topic at hand, it seems like there's a ton of proposals already made, and the odds of someone else solving this with another proposal that makes everyone go "Yes! That's it!" approach zero.

It seems to me perhaps the conversation should move in the direction of categorizing the various dimensions of the proposals made here, and getting a sense of the prioritization. I'd especially like to see this with an eye towards revealing contradictory requirements being vaguely applied by people here.

For instance, I've seen some complaints about additional jumps being added in the control flow. But for myself, in the parlance of the very original proposal, I value not having to add || &PathError{"chdir", dir, err} eight times within a function if they are common. (I know Go is not as allergic to repeated code as some other languages, but still, repeated code is at a very high risk for divergence bugs.) But pretty much by definition, if there is a mechanism for factoring out such error handling, the code can't flow from top-to-bottom, left-to-right, with no jumps. Which is generally considered more important? I suspect careful examination of the requirements people are implicitly placing on the code would reveal other mutually-contradictory requirements.

But just in general, I feel like if the community could agree on the requirements after all this analysis, the correct solution may very well just fall out of them clearly, or at the very least, the correct solution set will be so obviously constrained that the problem becomes tractable.

(I'd also point out that as this is a proposal, the current behavior should in general be subjected to the same analysis as the new proposals. The goal is significant improvement, not perfection; rejecting two or three significant improvements because none of them are perfect is a path to paralysis. All proposals are backwards compatible anyhow, so in those cases where the current approach is already the best anyhow (imho, the case where every error is handled legitimately differently, which in my experience is rare but happens), the current approach will still be available.)

I've been thinking on this since the second time I wrote if err !=nil in a function, it strikes me that a fairly simple solution would be to allow a conditional return that looks like the first part of ternary with the understanding being if the condition fails we don't return.

I'm not sure how well this would work in terms of parsing/compiling but seems it should be easy enough to interpret as an if statement where the '?' is seen without breaking compatibility where it is not seen, so thought I would throw it out there as an option.

Additionally there would be other uses for this beyond error handling.

so you might do something like this:

func example1() error {
    err := doSomething()
    return err != nil ? err
    //more code
}

func example2() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, err
    //more code
}

We could also do things like this when we have some clean up code, assuming that handleErr returned an err:

func example3() error {
    err := doSomething()
    return err !=nil ? handleErr(err)
    //more code
}

func example4() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, handleErr(err)
    //more code
}

Perhaps it then also follows on that you would be able to reduce this to a one liner if you wanted:

func example5() error {
    return err := doSomething(); err !=nil ? handleErr(err)
    //more code
}

func example6() (*Mything, error) {
    return err := doSomething(); err !=nil ? nil, handleErr(err)
    //more code
}

previous fetch example from @jba could potentially look like this:

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {

    return err := createFolderIfNotExist(to); err != nil ? nil, err
    return err := clearFolder(to); err != nil ? nil, err
    return err := cloneRepo(to, from); err != nil ? nil, err
    dirs, err := getContentFolders(to)

    return dirs, err
}

Would be interested in reactions to this suggestion, perhaps not a huge win in saving boilerplate but keeps quite explicit and hopefully only requires a small backwards compatible change (maybe some massively inaccurate assumptions on that front).

You could perhaps separate this out with a separate return? keyword which may add to clarity and make life simpler in terms of not having to worry about compatibility with return (thinking all the tooling), these could then just be internally rewritten as if/return statements, giving us this:

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {

    return? err := createFolderIfNotExist(to); err != nil ? nil, err
    return? err := clearFolder(to); err != nil ? nil, err
    return? err := cloneRepo(to, from); err != nil ? nil, err
    dirs, err := getContentFolders(to)

    return dirs, err
}

There doesn't seem to be much difference between

return err != nil ? err

and

 if err != nil { return err }

Also, sometimes you might want to do something other than return, like call panic or log.Fatal.

I've been spinning on this since I put in a proposal last week, and I've come to the conclusion that I agree with @thejerf: we've been discussing proposal after proposal without really taking a step back and examining what we like about each, dislike about each, and what priorities are for a "proper" solution.

The most commonly stated requirements are that at the end of the day, Go needs to be able to handle 4 cases of error handling:

  1. Ignoring the error
  2. Returning the error unmodified
  3. Returning the error with context added
  4. Panicking (or killing the program)

Proposals seem to fall into one of three categories:

  1. Revert to atry-catch-finally style of error handling.
  2. Add new syntax/builtins to handle all 4 cases listed above
  3. Asserting that go handles some cases well enough, and proposing syntax/builtins to help with the other cases.

Criticisms of the given proposals seem to be split between concerns over code readability, non-obvious jumps, implicit addition of variables to scopes, and conciseness. Personally, I think there has been a lot of personal opinion in the criticisms of proposals. I'm not saying that that is a bad thing, but it seems to me that there's not really an objective criteria to rate proposals against.

I'm probably not the person to try to create that list of criteria, but I think it would be very helpful if someone were to put that together. I've tried to outline my understanding of the debate so far as a starting point for breaking down 1. what we've seen, 2. what's wrong with it, 3. why those things are wrong, and 4. what we'd like to see instead. I think I captured a decent amount of the first 3 items, but I'm having trouble coming up with an answer for item 4 without resorting to "what Go currently has".

@jba has another nice summarization comment above, for more context. He says much of what I've said here, in different words.

@ianlancetaylor, or anyone else more closely involve with the project than I, would you feel comfortable adding a "formal" (all in one comment, organized, and somewhat comprehensive but in no way binding) set of criteria that need to be met? Maybe if we discuss those criteria and nail down 4-6 bullet points that proposals need to meet, we can re-boot the discussion with some more context?

I don't think I can write a comprehensive formal set of criteria. The best I can do is an incomplete list of things that matter that should only be disregarded if there is a significant benefit to doing so.

  • Good support for 1) ignoring an error; 2) returning an error unmodified; 3) wrapping an error with additional context.
  • While error handling code should be clear, it should not dominate the function. It should be easy to read the non-error handling code.
  • Existing Go 1 code should continue to work, or at the very least it must be possible to mechanically translate Go 1 to the new approach with complete reliability.
  • The new approach should encourage programmers to handle errors correctly. It should ideally be easy to do the right thing, whatever the right thing is in any situation.
  • Any new approach should be shorter and/or less repetitive than the current approach, while remaining clear.
  • The language does work today, and every change carries a cost. The benefit of the change must be clearly worth the cost. It should not just be a wash, it should be clearly better.

I hope to collect the notes at some point here myself, but I want to address what is IMHO another major stumbling block in this discussion, the triviality of the example(s).

I've extracted this from a real project of mine and scrubbed it for external release. (I think the stdlib is not the best source, as it is missing logging concerns, among others..)

func NewClient(...) (*Client, error) {
    listener, err := net.Listen("tcp4", listenAddr)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, err := ConnectionManager{}.connect(server, tlsConfig)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    if err != nil {
        return nil, err
    }

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    if err != nil {
        return nil, err
    }

    session, err := communicationProtocol.FinalProtocol(conn)
    if err != nil {
        return nil, err
    }
    client.session = session

    return client, nil
}

(Let's not bikeshed the code too much. I can't stop you of course, but remember this isn't my real code, and has been mangled a bit. And I can't stop you from posting your own sample code.)

Observations:

  1. This is not perfect code; I return bare errors a lot because it's so easy, exactly the sort of code we have issues with. Proposals should be scored both by conciseness and by how easily they demonstrate fixing this code.
  2. The if forwardPort == 0 clause _deliberately_ continues through errors, and yes, this is the real behavior, not something I added for this example.
  3. This code EITHER returns a valid, connected client OR it returns an error and no resource leaks, so the handling around .Close() (only if the function errors out) is deliberate. Note also the errors from Close disappear, as is quite typical in real Go.
  4. The port number is constrained elsewhere, so url.Parse can't fail (by examination).

I wouldn't claim this demonstrates every possible error behavior, but it does cover quite a gamut. (I often defend Go on HN and such by pointing out that by the time my code is done cooking, it is often the case in my network servers that I have _all kinds_ of erroring behaviors; examining my own production code, from 1/3rd to fully 1/2 of the errors did something other than simply get returned.)

I will also (re-)post my own (updated) proposal as applied to this code (unless someone convinces me they've got something even better before then), but in the interests of not monopolizing the conversation I'm going to wait at least over the weekend. (This is less text than it appear to be because it's a huge chunk of source, but still....)

Using try where try is just a shortcut for if != nil return reduces the code by 6 lines out of 59, which is about 10%.

func NewClient(...) (*Client, error) {
    listener, err := net.Listen("tcp4", listenAddr)
    try err

    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, err := ConnectionManager{}.connect(server, tlsConfig)
    try err

    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    try err

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    try err

    session, err := communicationProtocol.FinalProtocol(conn)
    try err

    client.session = session

    return client, nil
}

Notably, in several places I wanted to write try x() but I couldn't since I needed err to be set for the defers to work properly.

One more. If we have try be something that can happen on assignment lines, it goes down to 47 lines.

func NewClient(...) (*Client, error) {
    try listener, err := net.Listen("tcp4", listenAddr)

    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    try conn, err := ConnectionManager{}.connect(server, tlsConfig)

    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    try session, err := communicationProtocol.FinalProtocol(conn)

    client.session = session

    return client, nil
}
import "github.com/pkg/errors"

func Func3() (T1, T2, error) {...}

type PathError {
    err Error
    x   T3
    y   T4
}

type MiscError {
    x   T5
    y   T6
    err Error
}


func Foo() (T1, T2, error) {
    // Old school
    a, b, err := Func(3)
    if err != nil {
        return nil
    }

    // Simplest form.
    // If last unhandled arg's type is same 
    // as last param of func,
    // then use anon variable,
    // check and return
    a, b := Func3()
    /*    
    a, b, err := Func3()
    if err != nil {
         return T1{}, T2{}, err
    }
    */

    // Simple wrapper
    // If wrappers 1st param TypeOf Error - then pass last and only unhandled arg from Func3() there
    a, b, errors.WithStack() := Func3() 
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, errors.WithStack(err)
    }
    */

    // Bit more complex wrapper
    a, b, errors.WithMessage("unable to get a and b") := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, errors.WithMessage(err, "unable to get a and b")
    }
    */

    // More complex wrapper
    // If wrappers 1nd param TypeOf is not Error - then pass last and only unhandled arg from Func3() as last
    a, b, fmt.Errorf("at %v Func3() return error %v", time.Now()) := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, fmt.Errorf("at %v Func3() return error %v", time.Now(), err)
    }
    */

    // Wrapping with error types
    a, b, &PathError{x,y} := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, &PathError{err, x, y}
    }
    */
    a, b, &MiscError{x,y} := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, &MiscError{x, y, err}
    }
    */

    return a, b, nil
}

Slightly magic (feel free to -1) but supports mechanical translation

This is what (somewhat updated) my proposal would look like:

func NewClient(...) (*Client, error) {
    defer annotateError("client couldn't be created")

    listener := pop net.Listen("tcp4", listenAddr)
    defer closeOnErr(listener)
    conn := pop ConnectionManager{}.connect(server, tlsConfig)
    defer closeOnErr(conn)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
         forwardOut = forwarding.NewOut(pop url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort)))
    }

    client := &Client{listener: listener, conn: conn, forward: forwardOut}

    toServer := communicationProtocol.Wrap(conn)
    pop toServer.Send(&client.serverConfig)
    pop toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    session := pop communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

func closeOnErr(c io.Closer) {
    if err := erroring(); err != nil {
        closeErr := c.Close()
        if err != nil {
            seterr(multierror.Append(err, closeErr))
        }
    }
}

func annotateError(annotation string) {
    if err := erroring(); err != nil {
        log.Printf("%s: %v", annotation, err)
        seterr(errwrap.Wrapf(annotation +": {{err}}", err))
    }
}

Definitions:

pop is legal for function expressions where the right-most value is an error. It is defined as "if the error is not nil, return out of the function with the zero values for all other values in the results and that error, otherwise yield a set of values without the error". pop has no privileged interaction with erroring(); a normal return of an error will still be visible to erroring(). This also means you can return non-zero values for the other return values, and still use the deferred error handling. The metaphor is popping the rightmost element off the "list" of return values.

erroring() is defined as going up the stack to the deferred function being run, and then the previous stack element (the function the defer is running in, NewClient in this case), to access the value of the returned error currently in progress. If the function has no such parameter, either panic or return nil (whichever makes more sense). This error value need not have come from pop; it is anything that returns an error from the target function.

seterr(error) allows you to change the error value being returned. It will then be the error seen by any future erroring() calls, which as shown here allows the same defer-based chaining that can be done now.

I'm using the hashicorp wrapping and multierror here; insert your own clever packages as desired.

Even with the extra function defined, the sum is shorter. I expect to amortize the two functions across additional uses, so those should only count partially.

Observe I just leave the forwardPort handling alone, rather than try to jam some more syntax around it. As an exceptional case, it's OK for this to be more verbose.

The most interesting thing about this proposal IMHO can only be seen if you imagine trying to write this with conventional exceptions. It ends up getting fairly deeply nested, and handling the _collection_ of the errors that can occur is quite tedious with exception handling. (Just as in real Go code the .Close errors tend to be ignored, errors that happen in the exception handlers themselves tend to be ignored in exception-based code.)

This extends existing Go patterns such as defer and the use of errors-as-values to make easy correct error-handling patterns that in some cases are difficult to express either with current Go or exceptions, does not require radical surgery to the runtime (I don't think), and also, doesn't actually _require_ a Go 2.0.

Disadvantages include claiming erroring, pop, and seterr as keywords, incurring the defer overhead for these functionalities, the fact that factored error handling does jump around some to the handling functions, and that it doesn't do anything to "force" correct handling. Though I'm not sure the last is possible, since by the (correct) requirement to be backwards compatible, you can always do the current thing.

Very interesting discussion here.

I would like to keep the error variable on the left side so no magically appearing variables are introduced. Like the original proposal I would like the handling of the error stuff on the same line. I wouldn't use the ||-operator as it looks "too boolean" for me and somehow hides the "return".

So I would make it more readable by using the extended keyword "return?". In C# the question mark is used at some places to make shortcuts. E. g. instead of writing:

if(foo != null)
{ foo.Bar(); }

you can just write:
foo?.Bar();

So for Go 2 I would like to propose this solution:

func foobar() error {
    return fmt.Errorf("Some error happened")
}

// Implicitly return err (there must be exactly one error variable on the left side)
err := foobar() return?
// Explicitly return err
err := foobar() return? err
// Return extended error info
err := foobar() return? &PathError{"chdir", dir, err}
// Return tuple
err := foobar() return? -1, err
// Return result of function (e. g. for logging the error)
err := foobar() return? handleError(err)
// This doesn't compile as you ignore the error intentionally
foobar() return?

Just a thought:

foo, err := myFunc()
err != nil ? return wrap(err)

Or

if err != nil ? return wrap(err)

If you're willing to put some curly braces around that, we don't need to change anything!

if err != nil { return wrap(err) }

You can have _all_ the custom handling you want (or none at all), you've saved two lines of code from the typical case, it's 100% backwards compatible (because there's no changes to the language), it's more compact, and it's easy. It hits many of Ian's driving points. The only change might be the gofmt tooling?

I wrote this before reading carlmjohnson's suggestion, which is similar...

Just a # before an error.

But in a real world application you would still have to write the normal if err != nil { ... } so you can log errors, this makes the minimalistic error handling useless, Unless you could add a return middleware through annotations called after, which runs after the functions returns... (like defer but with args).

@after(func (data string, err error) {
  if err != nil {
    log.Error("error", data, " - ", err)
  }
})
func alo() (string, error) {
  // this is the equivalent of
  // data, err := db.Find()
  // if err != nil { 
  //   return "", err 
  // }
  str, #err := db.Find()

  // ...

  #err = db.Create()

  // ...

  return str, nil
}

func alo2() ([]byte, []byte, error, error) {
  // this is the equivalent of
  // data, data2, err, errNotFound := db.Find()
  // if err != nil { 
  //   return nil, nil, err, nil
  // } else if errNotFound != nil {
  //   return nil, nil, nil, errNotFound
  // }
  data, data2, #err, #errNotFound := db.Find()

  // ...

  return data, data2, nil, nil
}

cleaner than:

func alo() (string, error) {
  str, err := db.Find()
  if err != nil {
    log.Error("error on find in database", err)
    return "", err
  }

  // ...

  if err := db.Create(); err != nil {
    log.Error("error on create", err)
    return "", err
  }

  // ...

  return str, nil
}

func alo2() ([]byte, []byte, error, error) {
  data, data2, err, errNotFound := db.Find()
  if err != nil { 
    return nil, nil, err, nil
  } else if errNotFound != nil {
    return nil, nil, nil, errNotFound
  }

  // ...

  return data, data2, nil, nil
}

How about a Swift like guard statement, except instead of guard...else it's guard...return:

file, err := os.Open("fails.txt")
guard err return &FooError{"Couldn't foo fails.txt", err}

guard os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

I like the explicitness of go error handling. The only trouble, imho, is how much space it takes. I would suggest 2 tweaks:

  1. allow nil-able items to be used in a boolean context, where nil is equivalent to false, non-nil to true
  2. support single-line conditional statement operators, like && and ||

So

file, err := os.Open("fails.txt")
if err != nil {
    return &FooError{"Couldn't foo fails.txt", err}
}

can become

file, err := os.Open("fails.txt")
if err {
    return &FooError{"Couldn't foo fails.txt", err}
}

or even shorter

file, err := os.Open("fails.txt")
err && return &FooError{"Couldn't foo fails.txt", err}

and, we can do

i,ok := v.(int)
ok || return fmt.Errorf("not a number")

or, perhaps

i,ok := v.(int)
ok && s *= i

If overloading && and || creates too much ambiguity, perhaps some other characters (no more than 2) can be selected, e.g. ? and # or ?? and ##, or ?? and !!, whatever. The point being to support a single-line conditional statement with minimal "noisy" characters (no needed parens, braces, etc.). The operators && and || are good because this usage has precedent in other languages.

This is not a proposal to support complex single-line conditional expressions, only single-line conditional statements.

Also, this is not a proposal to support a full range of "truthiness" ala some other languages. These conditionals would only support nil/non-nil, or booleans.

For these conditional operators, it may even be suitable to restrict to single variables and not support expressions. Anything more complex, or with an else clause would be handled with standard if ... constructs.

Why just don't invent a wheel and use known try..catch form as @mattn said before? )

try {
    a := foo() // func foo(string, error)
    b := bar() // func bar(string, error)
} catch (err) {
    // handle error
}

It seems like there is no reason to distinguish the catched error source, because if you really need this you could always use the old form of if err != nil without try..catch.

Also, I do not really sure about that, but may be add the ability to "throw" an error if it does not handled?

func foo() (string, error) {
    f := bar() // similar to if err != nil { return "", err }
}

func baz() string {
    // Compilation error.
    // bar's error must be handled because baz() does not return error.
    return bar()
}

@gobwas from readability point of view it's very important to completely understand control flow. Looking at your example there's no telling, which line might cause a jump to the catch block. It's like a hidden goto statement. No wonder modern languages try to be explicit about it and require programmer to explicitly mark places where control flow might diverge due to an error being thrown. Pretty much like return or goto but with much nicer syntax.

@creker yes, I am totally agree with you. I was thinking about flow control in example above, but did not realize how to do this in a simple form.

Maybe something like:

try {
    a ::= foo() // func foo(string, error)
    b ::= bar() // func bar(string, error)
} catch (err) {
    // handle error
}

Or other suggestions before like try a := foo() ..?

@gobwas

When I land in the catch block, how do I know which function in the try block caused the error?

@urandom if you need to know it you probably want to do if err != nil without try..catch.

@robert-wallis: I mentioned Swift's guard statement earlier in the thread but the page is so big Github no longer loads it by default. :-P I still think that's a good idea, and in general, I support looking at other languages for positive/negative examples.

@pdk

allow nil-able items to be used in a boolean context, where nil is equivalent to false, non-nil to true

I see this leading to a lot of bugs using the flag package where people will write if myflag { ... } but mean to write if *myflag { ... } and it won't be caught by the compiler.

try/catch is only shorter than if/else when you try multiple things back to back, which is something more or less everyone agrees is bad because of the control flow issues, etc.

FWIW, Swift's try/catch at least resolves the visual problem of not knowing which statements might throw:

do {
    let dragon = try summonDefaultDragon() 
    try dragon.breathFire()
} catch DragonError.dragonIsMissing {
    // ...
} catch DragonError.halatosis {
    // ...
}

@robert-wallis , you have an example:

file, err := os.Open("fails.txt")
guard err return &FooError{"Couldn't foo fails.txt", err}

guard os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

On the first use of guard, it looks an awful lot like if err != nil { return &FooError{"Couldn't foo fails.txt", err}}, so I'm not sure if that's a big win.

On the second use, it's not immediately clear where the err comes from. It almost looks like it's what's returned from os.Open, which I'm guessing was not your intent? Would this be more accurate?

guard err = os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

In which case, it looks like ...

if err = os.Remove("fails.txt"); err != nil { return &FooError{"Couldn't remove fails.txt", err}}

But it still has less visual clutter. if err =, ; err != nil { - even if it's a one liner there's still too much going on for such a simple thing

Agreed that there is less clutter. But appreciably less in order to warrant adding to the language? I'm not sure that I agree there.

I think that there readability of try-catch blocks in Java/C#/... is very good as you can follow the "happy path" sequence without any interruption by the error handling. The downside is that you basically have a hidden goto mechanism.

In Go I start to insert empty lines after the error handler to make the continuation of the "happy path" logic more visible. So out of this sample from golang.org (9 lines)

record := new(Record)
err := datastore.Get(c, key, record) 
if err != nil {
    return &appError{err, "Record not found", 404}
}
err := viewTemplate.Execute(w, record)
if err != nil {
    return &appError{err, "Can't display record", 500}
}

i often do that (11 lines)

record := new(Record)

err := datastore.Get(c, key, record) 
if err != nil {
    return &appError{err, "Record not found", 404}
}

err := viewTemplate.Execute(w, record)
if err != nil {
    return &appError{err, "Can't display record", 500}
}

Now back to the proposal, as I already posted something like this would be nice (3 lines)

record := new(Record)
err := datastore.Get(c, key, record) return? &appError{err, "Record not found", 404}
err := viewTemplate.Execute(w, record) return? &appError{err, "Can't display record", 500}

Now I see the happy path clearly. My eyes are still aware that on the right side there is code for error handling but I only need to "eye-parse" it when really necessary.

A question to all on the side: should this code compile?

func foobar() error {
    return fmt.Errorf("Some error")
}
func main() {
    foobar()
}

IMHO the user should be forced to say that he intentionally ignores the error with:

func main() {
    _ := foobar()
}

Adding a mini experience report related to point 3 of @ianlancetaylor original post, return the error with additional contextual information.

When developing a flac library for Go, we wanted to add contextual information to errors using @davecheney pkg/errors package (https://github.com/mewkiz/flac/issues/22). More specifically, we wrap errors returned using errors.WithStack which annotates errors with stack trace information.

Since the error is annotated, it needs to create a new underlying type to store this additional information, in the case of errors.WithStack, the type is errors.withStack.

type withStack struct {
    error
    *stack
}

Now, to retrieve the original error, the convention is to use errors.Cause. This lets you compare the original error against for instance io.EOF.

A user of the library may then write something along the lines of https://github.com/mewkiz/flac/blob/0884ed715ef801ce2ce0c262d1e674fdda6c3d94/cmd/flac2wav/flac2wav.go#L78 using errors.Cause to check the original error value:

frame, err := stream.ParseNext()
if err != nil {
    if errors.Cause(err) == io.EOF {
        break
    }
    return errors.WithStack(err)
}

This works well in almost all cases.

When refactoring our error handling to make consistent use of pkg/errors throughout for added context information, we ran into a rather serious issue though. To validate zero-padding we've implemented an io.Reader which simply checks if the read bytes are zero, and reports an error otherwise. The problem is that having performed an automatic refactoring to add contextual information to our errors, suddenly our test cases started failing.

The problem was that the underlying type of the error returned by zeros.Read now is errors.withStack, rather than io.EOF. Thus subsequently caused issues when we used that reader in combination with io.Copy, which checks for io.EOF specifically, and does not know to use errors.Cause to "unwrap" an error annotated with contextual information. Since we cannot update the standard library, the solution was to return the error without annotated information (https://github.com/mewkiz/flac/commit/6805a34d854d57b12f72fd74304ac296fd0c07be).

While loosing the annotated information for interfaces which return concrete values is a loss, it is possible to live with.

The take away from our experience has been that we were lucky, as our test cases caught this. The compiler did not produce any errors, since the zeros type still implements the io.Reader interface. We also did not think that we would hit an issue, as the added error annotation was a machine generated rewrite, simply add contextual information to errors should not affect the program behaviour in a normal state.

But it did, and for this reason, we wish to contribute our experience report for consideration; when thinking of how to integrate addition of contextual information into error handling for Go 2, such that error comparison (as used in interface contracts) still holds seamlessly.

Kindly,
Robin

@mewmew, let's keep this issue about the control flow aspects of error handling. How best to wrap and unwrap errors should be discussed elsewhere, since it's largely orthogonal to control flow.

I'm not familiar with your codebase, and I realize you said that it was an automated refactoring, but why did you need to include contextual information with EOF? Although it's treated as an error by the type system, EOF is really more of a signal value, not an actual error. In an io.Reader implementation in particular, it's an expected value most of the time. Wouldn't a better fix to have been to only wrap the error if it wasn't io.EOF?

Yeah, I propose that we just leave things the way they are. I was under the impression that Go's error system was deliberately designed this way to discourage developers from punting errors up the call stack. That errors are meant to be resolved where they occur, and to know when its more appropriate to use panic when you cant.

I mean isn't try-catch-throw essentially the same behavior of panic() and recover() anyways?

Sigh, if we are really going to start trying to go down this road. Why can't we just do something like

_, ? := foo()
x?, err? := bar()

or perhaps even something like

_, err := foo(); return err?
x, err := bar(); return x? || err?
x, y, err := baz(); return (x? && y?) || err?

where the ? becomes a shorthand alias for the if var != nil{ return var }.

We can even define another special builtin interface that is satisfied by the method

func ?() bool //looks funky but avoids breakage.

that we can use to essentially override the default behavior of the new and improved conditional operator.

@mortdeus

I think I agree.
If the problem is to have a nice way to present the happy path, a plugin for an IDE could fold/unfold every instance of if err != nil { return [...] } with a shortcut?

I feel like every part now is important. err != nil is important. return ... is important.
It's a bit of a hassle to write, but it has to be written. And does it really slow down people? What take time is thinking about the error and what to return, not writing it.

I would be way more interested in a proposal that allow to limit the scope of the err variable.

I think my conditional idea is the neatest way to resolve this issue. I just thought of a few other things that would make this feature worthy enough to include in Go. I'm going to write up my idea in a separate proposal.

I don't see how this could work:

x, y, err := baz(); return (x? && y?) || err?

where the ? becomes a shorthand alias for the if var == nil{ return var }.

x, y, err := baz(); return (if x == nil{ return x} && if y== nil{ return y}) || if err == nil{ return err}

x, y, err := baz(); return (x? && y?) || err?

becomes

x, y, err := baz();
if ((x != nil && y !=nil) || err !=nil)){
return x,y, err
}

when you see x? && y? || err? you should be thinking "is x and y valid? What about err?"

if not then the return function doesn't execute. I just wrote up a new proposal on this idea that takes the idea a bit further with a new special builtin interface type

I suggest Go add default err handling in Go version 2.

If user does not handle error, compiler return err if it is not nil, so if user write:

func Func() error {
    func1()
    func2()
    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

compile transform it to:

func Func() error {
    err := func1()
    if err != nil {
        return err
    }

    err = func2()
    if err != nil {
        return err
    }

    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

If user handle the err or ignore it using _, compiler will do nothing:

_ = func1()

or

err := func1()

for multiple return values, it's similar:

func Func() (*YYY, error) {
    ch, x := func1()
    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

compiler will transform to:

func Func() (*YYY, error) {
    ch, x, err := func1()
    if err != nil {
        return nil, err
    }

    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

If Func()'s signature doesn't return error but call functions which return error, compiler will report Error: "Please handle your error in Func()"
Then user can just log the err in Func()

And if user want to wrap some info to the err:

func Func() (*YYY, error) {
    ch, x := func1() ? Wrap(err, "xxxxx", ch, "abc", ...)
    return yyy, nil
}

or

func Func() (*YYY, error) {
    ch, x := func1() ? errors.New("another error")
    return yyy, nil
}

The benefit is,

  1. the program just fail at the point where error happens, user can not ignore error implicitly.
  2. can reduce lines of code notably.

its not that easy because Go can have multiple return values and the language shouldn't assign what essentially equates to default values for return arguments without the developer being explicitly aware what is going on.

I believe that putting the error handling in the assignment syntax doesn't solve the root of the problem, which is "error handling is repetitive".

Using if (err != nil) { return nil } (or similar) after many lines of code (where it makes sense to do so) goes against the DRY (don't repeat yourself) principal. I believe that's why we don't like this.

There are also problems with try ... catch. You don't need to explicitly handle the error in the same function where it happens. I believe that's a notable reason why we don't like try...catch.

I don't believe these are mutually exclusive; we can have a sort of try...catch without a throws.

Another thing I personally dislike about try...catch is the arbitrary necessity of the try keyword. There's no reason you can't catch after any scope limiter, as far as a working grammar is concerned. (somebody call it out if I'm wrong about this)

This is what I propose:

  • using ? as the placeholder for a returned error, where _ would be used to ignore it
  • instead of catch as in my example below, error? could be used instead for full backwards compatibility

^ If my assumption that these are backwards-compatible is incorrect please call it out.

func example() {
    {
        // The following line will invoke the catch block
        data, ? := foopkg.buyIt()
        // The next two lines handle an error differently
        otherData, err := foopkg.useIt()
        if err != nil {
            // Here we eliminated deeper indentation
            otherData, ? = foopkg.breakIt()
        }
        if data == "" || otherData == "" {
        }
    } catch (err) {
        return errors.Label("fix it", err)
        // Aside: why not add Label() to the error package?
    }
}

I thought of an argument against this: if you write it this way, changing that catch block might have unintended affects on code in deeper scopes. This is that same problem we have with try...catch.

I think if you can only do this in the scope of a single function, the risk is manageable - possibly the same as the current risk of forgetting to change a line of error-handling code when you intend to change many of them. I see this as the same difference between the consequences of code-reuse, and the consequences of not following DRY (i.e. no free lunch, as they say)

Edit: I forgot to specify an important behaviour for my example. In the case that ? is used in a scope without any catch, I think this should be a compiler error (rather than throwing a panic, which was admittedly the first thing I thought of)

Edit 2: Crazy idea: maybe the catch block just wouldn't affect the control flow... it would literally be like copying and pasting the code inside catch { ... } to the line after the error is ?ed (well not quite - it would still have its own scope). It seems weird since none of us are used to it, so catch definitely shouldn't be the keyword if it's done this way, but otherwise... why not?

@mewmew, let's keep this issue about the control flow aspects of error handling. How best to wrap and unwrap errors should be discussed elsewhere, since it's largely orthogonal to control flow.

Ok, lets keep this thread to control flow. I added it simply because it was an issue related to concrete usage of point 3 return the error with additional contextual information.

@jba Do you know of any issue specifically dedicated to wrapping/unwrapping of contextual information for errors?

I'm not familiar with your codebase, and I realize you said that it was an automated refactoring, but why did you need to include contextual information with EOF? Although it's treated as an error by the type system, EOF is really more of a signal value, not an actual error. In an io.Reader implementation in particular, it's an expected value most of the time. Wouldn't a better fix to have been to only wrap the error if it wasn't io.EOF?

@DeedleFake I can elaborate a bit, but to stay on topic will do so in the aforementioned issue dedicated to wrapping/unwrapping of contextual information for errors.

The more I read all the proposals (including mine) the less I think we really have a problem with error handling in go.

What I would like is some enforcement to not accidentally ignore an error return value but enforce at least
_ := returnsError()

I know there is tooling to find these issues, but a first level support of the language could catch some bugs. Not handling an error at all is like having an unused variable for me - which already is an error. It would also help with refactoring, when you introduce an error return type to a function, since you are forced to handle it in all places.

The main issue that most people try to solve here seems to be the "amount of typing" or "number of lines". I would agree with any syntax that reduces the number of lines, but that's mostly a gofmt issue. Just allow inline "single line scopes" and we are good.

Another suggestion to save some typing is implicit nil checking like with booleans:

err := returnsError()
if err { return err }

or even

if err := returnsError(); err { return err }

That would work with all pointer types of cause.

My feeling is that everything that reduces the function call + error handling into a single line will lead to less readable code and more complex syntax.

less readable code and more complex syntax.

We already have less readable code because of the verbose error handling. Adding already mentioned Scanner API trick, that supposed to hide that verbosity, makes it even worse. Adding more complex syntax might help with readability, that what syntactic sugar is for in the end. Otherwise there's no point in this discussion. The pattern of bubbling up an error and returning zero-value for everything else is common enough to warrant a language change, in my opinion.

The pattern of bubbling up an error and returning zero-value for everything

19642 would make this easier.


Also, thanks @mewmew for the experience report. It is definitely related to this thread, insofar as it relates to dangers in particular kinds of error handling designs. I'd love to see more of these.

I don't feel like I explained my idea very well, so I created a gist (and revised many of the shortcomings that I just noticed)

https://gist.github.com/KernelDeimos/384aabd36e1789efe8cbce3c17ffa390

There's more than one idea in this gist, so I hope they can be discussed separate from each other

Setting aside for a moment the idea that the proposal here has to be explicitly about error handling, what if Go introduced something like a collect statement?

A collect statement would be of the form collect [IDENT] [BLOCK STMT], where ident must me an in-scope variable of a nil-able type. Within a collect statement, a special variable _! is available as an alias for the variable being collected to. _! cannot be used anywhere but as an assignment, same as _. Whenever _! is assigned to, an implicit nil check is performed, and if _! is not nil, the block ceases execution and continues with the rest of the code.

Theoretically, this would look something like this:

func TryComplexOperation() (*Result, error) {
    var result *Result
    var err error

    collect err {
        intermediate1, _! := Step1()
        intermediate2, _! := Step2(intermediate1, "something")
        // assign to result from the outer scope
        result, _! = Step3(intermediate2, 12)
    }
    return result, err
}

which is equivalent to

func TryComplexOperation() (*Result, error) {
    var result *Result
    var err error

    {
        var intermediate1 SomeType
        intermediate1, err = Step1()
        if err != nil { goto collectEnd }

        var intermediate2 SomeOtherType
        intermediate2, err = Step2(intermediate1, "something")
        if err != nil { goto collectEnd }

        result, err = Step3(intermediate2, 12)
        // if err != nil { goto collectEnd }, but since we're at the end already we can omit this
    }

collectEnd:
    return result, err
}

Some nice other things a syntax feature like this would enable:

// try several approaches for acquiring a value
func GetSomething() (s *Something) {
    collect s {
        _! = fetchOrNil1()
        _! = fetchOrNil2()
        _! = new(Something)
    }
    return s
}

New syntax features required:

  1. keyword collect
  2. special ident _! (I've played with this in the parser, it's not hard to make this match as an ident without breaking anything else)

The reason I'm suggesting something like this is because the "error handling is too repetitive" argument can be boiled down to "nil checks are too repetitive". Go already has plenty of error handling features that work as-is. You can ignore an error with _ (or just not capture return values), you can return an error unmodified with if err != nil { return err }, or add context and return with if err != nil { return wrap(err) }. None of those methods, on their own, are too repetitive. The repetitiveness (obviously) comes from having to repeat these or similar syntax statements all over the code. I think introducing a way to execute statements until a non-nil value is encountered is a good way to keep error handling the same, but reduce the amount of boilerplate necessary to do so.

  • Good support for 1) ignoring an error; 2) returning an error unmodified; 3) wrapping an error with additional context.

check, since it stays the same (mostly)

  • While error handling code should be clear, it should not dominate the function. It should be easy to read the non-error handling code.

check, as the error handling code can now go in one place if needed, while the meat of the function can happen in a linearly readable way

  • Existing Go 1 code should continue to work, or at the very least it must be possible to mechanically translate Go 1 to the new approach with complete reliability.

check, this is an addition and not an alteration

  • The new approach should encourage programmers to handle errors correctly. It should ideally be easy to do the right thing, whatever the right thing is in any situation.

check, I think, since the mechanisms for error handling aren't different - we'd just have a syntax for "collecting" the first non-nil value from a series of executions and assignments, which can be used to limit the number of places we have to write our error handling code in a function

  • Any new approach should be shorter and/or less repetitive than the current approach, while remaining clear.

I'm not sure this applies here, since the feature suggested applies to more than just error handling. I do think it can shorten and clarify code that can generate errors, without cluttering in nil checks and early returns

  • The language does work today, and every change carries a cost. The benefit of the change must be clearly worth the cost. It should not just be a wash, it should be clearly better.

Agreed, and so it seems that a change whose scope extends beyond just error handling may be appropriate. I believe the underlying issue is that nil checks in go become repetitive and verbose, and it just so happens error is a nil-able type.

@KernelDeimos We essentially just came up with the same thing. However, I took it one step further and explained why the x, ? := doSomething() way doesn't really work out so well in practice. Though its neat to see that I am not the only person who is thinking about adding the ? operator into the language in an interesting way.

https://github.com/golang/go/issues/25582

Isn't this basically just trap?

Here's a spitball:

func NewClient(...) (*Client, error) {
    trap(err error) {
        return nil, err
    }

    listener, err? := net.Listen("tcp4", listenAddr)
    trap(_ error) {
        listener.Close()
    }

    conn, err? := ConnectionManager{}.connect(server, tlsConfig)
    trap(_ error) {
        conn.Close()
    }

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    toServer.Send(&client.serverConfig)?

    toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})?

    session, err? := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

59 lines → 44

trap means "run this code in stack order if a variable marked with ? of specified type is not the zero value." It's like defer but it can affect control flow.

I kind of like the trap idea, but the syntax bugs me a bit. What if it was a type of declaration? For example, trap err error {} declares a trap called err of type error which, when assigned to, runs the given code. The code doesn't even have to return; it's just allowed to do so. This also breaks the dependance on nil being special.

Edit: Expanding and giving an example now that I'm not on a phone.

func Example(r io.Reader) error {
  trap err error {
    if err != nil {
      return err
    }
  }

  n, err? := io.Copy(ioutil.Discard, r)
  fmt.Printf("Read %v bytes.\n", n)
}

Essentially, a trap functions just like a var, except that whenever it's assigned to with the ? operator attached to it the code block is executed. The ? operator also prevents it from being shadowed when used with :=. Redeclaring a trap in the same scope, unlike a var, is allowed, but it must be of the same type as the existing one; this allows you to change the associated code block. Because the block that's running doesn't necessarily return, it also allows you to have separate paths for specific things, such as checking if err == io.EOF.

What I like about this approach is that it seems similar to the errWriter example from Errors are Values, but in a somewhat more generic setup that doesn't require declaration of a new type.

@carlmjohnson who were you replying to?
Regardless, this trap concept seems to be just a different way to write a defer statement, no? The code, as written, would be essentially the same if you panic'd on a non-nil error and then used a deferred closure to set named return values and perform cleanup. I think this runs into the same issues as my earlier proposal of using _! to automatically panic, as it puts one error handling method over another. FWIW I also felt that the code, as written, was much harder to reason about than the original. Could this trap concept be mimicked with go today, even if less clear than having syntax for it? I feel like it could and it would be if err != nil { panic (err) } and defer to capture and handle that.

It seems similar to the concept of the collect block I suggested above, which I personally feel provides a cleaner way to express the same idea ("if this value isn't nil, I want to capture it and do something with it"). Go likes to be linear, and explicit. trap feels like new syntax for panic/defer but with a less clear control flow.

@mccolljr, it appeared to be a reply to me. I infer from your post that you did not see my proposal (now in the "hidden items", although not that far up there), because I actually used a defer statement in my proposal, extended with a recover-like function for error handling.

I'd also observe the "trap" rewrite dropped a lot of functionality my proposal had (_very_ different errors come out), and additionally, it is unclear to me how to factor out error handling with the trap statements. A lot of that reduction from my proposal comes in the form of dropping error handling correctness and, I believe, returning to making it easier to just return errors directly than do anything else.

The ability to continue flow is allowed by the modified trap example that I gave above. I edited it in later, so I don't know if you saw it or not. It's very similar to a collect, but I think it gives a bit more control over it. Depending on how the scoping rules for it worked, it could be a bit spaghetti-ish, but I think it would be possible to find a good balance.

@thejerf Ah, that makes more sense. I didn't realize it was a reply to your proposal. However, It's not clear to me what the difference between erroring() and recover() would be, aside from the fact that recover responds to panic. Seems that we'd just be implicitly doing some form of panicking when an error needs to be returned. Deferring is also a somewhat costly operation so I'm not sure how I feel about using it in all functions that might generate errors.

@DeedleFake Same goes for trap, because the way I see it trap is either essentially a macro that inserts code when the ? operator is used which presents its own set of concerns and considerations, or it's implemented as a goto... which, what if the user doesn't return in the trap block, or it's just a syntactically different defer. Also, what if I declare several trap blocks in a function? Is that allowed? If so, which one gets executed? That adds implementation complexity. Go likes to be opinionated, and I like that about it. I think collect or a similar linear construct is more aligned with Go's ideology than trap, which, as was pointed out to me after my first proposal, seems to be a try-catch construct in costume.

what if the user doesn't return in the trap block

If the trap doesn't return or otherwise modify control flow (goto, continue, break, etc.), control flow returns to where the code block was 'called' from. The block itself would work similarly to calling a closure, with the exception that it has access to control flow mechanisms. The mechanisms would work in the place that the block is declared, not in the place where it is called from, so

for {
  trap err error {
    break
  }

  err? = errors.New("Example")
}

would work.

Also, what if I declare several trap blocks in a function? Is that allowed? If so, which one gets executed?

Yes, that's allowed. The blocks are named by the trap, so it's fairly straightforward figuring out which one should be called. For example, in

trap err error {
  // Block 1.
}

trap n int {
  // Block 2.
}

n? = 3

block 2 gets called. The big question in that case would probably be what happens in the case of n?, err? = 3, errors.New("Example"), which would probably require the order of assignments to be specified, as came up in #25609.

I think collect or a similar linear construct is more aligned with Go's ideology than trap, which, as was pointed out to me after my first proposal, seems to be a try-catch construct in costume.

I think both collect and trap are essentially try-catchs in reverse. A standard try-catch is a fail-by-default policy that requires you to check or it explodes. This is a succeed-by-default system that allows you to specify a failure path, essentially.

One thing that complicates the whole thing is the fact that errors are not inherently treated as a failure, and some errors, such as io.EOF, don't really specify failure at all. I think that's why systems that aren't tied to errors specifically, such as collect or trap, are the way to go.

"Ah, that makes more sense. I didn't realize it was a reply to your proposal. However, It's not clear to me what the difference between erroring() and recover() would be, aside from the fact that recover responds to panic."

Not having a great deal of difference is the point. I'm trying to minimize the number of new concepts created while getting as much power as possible from them. I consider building on existing functionality a feature, not a bug.

One of the points of my proposal is to explore beyond just "what if we fix this recurring chunk of three lines where we return err and replace it with a ?" to "how does it affect the rest of the language? What new patterns does it enable? What new 'best practices' does it create? What old 'best practices' cease to be best practices?" I'm not saying I finished that job. And even if it is judged the idea actually has too much power for Go's taste (as Go is not a power-maximizing language, and even with the design choice to limit it to type error it's still probably the most powerful proposal made in this thread, which I fully mean in both the good and bad senses of "powerful"), I think we could stand to be exploring the questions of what the new constructs will do to programs as a whole, rather than what it will do to seven line example functions, which is why I tried to at least bring the examples up to the ~50-100 lines of "real code" range. Everything looks the same in 5 lines, which includes the Go 1.0 error handling, which is perhaps part of the reason we all know from our own experiences there's a real problem here, but the conversation just sort of goes around in circles if we talk about it in too small a scale until some people start convincing themselves maybe there isn't a problem after all. (Trust your real coding experiences, not the 5-line samples!)

"Seems that we'd just be implicitly doing some form of panicking when an error needs to be returned."

It is not implicit. It is explicit. You use the pop operator when it does what you want. When it doesn't do what you want, you don't use it. What it does is simple enough to capture in a single simple sentence, though the specification would probably take a full paragraph as that is how such things work. There is no implicit. Further it is not a panic because it only unwinds one level of the stack, exactly like a return; it is as much a panic as a return is, which is not at all.

I also don't care if you spell pop as ? or whatever. I personally think a word looks a bit more Go-like as Go is not currently a symbol-rich language, but can't deny that a symbol has an advantage that it doesn't conflict with any existing source code. I'm interested in the semantics and what we can build on them and what behaviors the new semantics afford for programmers both new and experienced moreso than the spelling.

"Deferring is also a somewhat costly operation so I'm not sure how I feel about using it in all functions that might generate errors."

I already acknowledged that. Though I'd suggest that in general it's not that expensive and I don't feel too bad about saying that for optimization purposes if you've got a hot function, write it the current way. It is explicitly not my goal to try to modify 100% of all error handling functions, but to make 80% of them much simpler and more correct and let the 20% cases (probably honestly more like 98/2, honestly) stay as they are. The vast bulk of Go code is not sensitive to using defer, which is, after all, why defer exists in the first place.

In fact, you can trivially modify the proposal to not use defer and use some keyword like trap as a declaration that gets run only once regardless of where it appears, rather than the way defer is actually a statement that pushes a handler onto the stack of deferred functions. I deliberately chose to reuse defer to avoid adding new concepts to the language... even understanding the traps that could result from defers in loops unexpectedly biting people. But it's still just the one defer concept to understand.

Just to make it clear that adding a new keyword to the language is a breaking change.

package main

import (
    "fmt"
)

func return(i int)int{
    return i
}

func main() {
    return(1)
}

results in

prog.go:7:6: syntax error: unexpected select, expecting name or (

Which means that if we try to add a try, trap, assert, any keyword into the language we are running the risk of breaking a ton of code. Code that may longer be maintained.

That is why I initially proposed adding a special ? go operator that can be applied to variables in the context of statements. The ? character as of right now is designated as illegal character for variable names. Which means that it is not currently in use in any current Go code and therefore we can introduce it without incurring any breaking changes.

Now the issue of using it in the left side of an assignment is that it doesn't take into consideration that Go allows for multiple return arguments.

For example consider this function

func getCoord() x int, y int, z int, err error{
    x, err = getX()
    if err != nil{
        return 
    }

    y, err = getY()
    if err != nil{
        return 
    }

    z, err = getZ()
        if err != nil{
        return 
    }
    return
}

if we use ? or try in the lhs of assignment to get rid of the if err != nil blocks, do we automatically presume that errors mean all other values are now trash? What if we did it like this

func GetCoord() (x, y, z int, err error) {
    err = try GetX(&x) // or err? = GetX(&x) 
    err = try GetY(&y) // or err? = GetY(&x) 
    err = try GetZ(&z) // or err? = GetZ(&x) 
}

what assumptions do we make here? That it shouldn't be harmful to just assume it's okay to throw away the value? what if the error is meant to be more of a warning and the value x is fine? What if the only function that throws the err is the call to GetZ() and the x, y values are actually good? Do we presume to return them. What if we don't use named return args? What if the return args are reference types like a map or a channel, Should we presume it's safe to return nil to the caller?

TLDR; adding ? or try to assignments in an effort to eliminate

if err != nil{
    return err
}

introduces way too much confusion than perks.

And adding something like the trap suggestion introduces the possibility for breakage.

Which is why in my proposal that I made in a separate issue. I allowed for the ability to declare a func ?() bool on any type so that when you called say

x, err := doSomething; return x, err?    

you can have that trap side effect happen in a way that applies to any type.

And applying the ? to work only on statements like I showed allows for the programability of statements. In my proposal I suggest allowing for a special switch statement that allows for somebody to switch over cases that are the keyword + ?

switch {
    case select?:
    //side effect/trap code specific to select
    case return?:
    //side effect/trap code specific to returns
    case for?: 
    //side effect/trap code specific to for? 

    //etc...
}  

If we are using ? on a type that doesn't have an explicit ? function declared or a builtin type, then the default behavior of checking if var == nil || zero'd value {execute the statement} is the presumed intent.

Idk, im not an expert in programming language design, but isn't this

For example, the os.Chdir function is currently

func Chdir(dir string) error {
  if e := syscall.Chdir(dir); e != nil {
      return &PathError{"chdir", dir, e}
  }
  return nil
}

Under this proposal, it could be written as

func Chdir(dir string) error {
  syscall.Chdir(dir) || &PathError{"chdir", dir, err}
  return nil
}

essentially the same thing as javascript's arrow functions or as Dart defines it "fat arrow syntax"

e.g.

func Chdir(dir string) error {
    syscall.Chdir(dir) => &PathError{"chdir", dir, err}
    return nil
}

from the dart tour.

For functions that contain just one expression, you can use a shorthand syntax:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
The => expr syntax is a shorthand for { return expr; }. The => notation is sometimes referred to as fat arrow syntax.

@mortdeus, the left side of the Dart arrow is a function signature, whereas syscall.Chdir(dir) is an expression. They seem more or less unrelated.

@mortdeus I forgot to clarify earlier, but the idea I commented here is hardly similar to the proposal you tagged. I do like the idea of ? as a placeholder so I copied that, but my idea emphasised re-using a single code block to handle the errors while avoiding some of the known problems with try...catch. I was very careful to come up with something that wasn't previously talked about so I could contribute a new idea.

How about a new conditional return (or returnIf) statement?

return(bool expression) ...

ie.

err := syscall.Chdir(dir)
return(err != nil) &PathError{"chdir", dir, e}

a, b, err := Foo()    // signature: func Foo() (string, string, error)
return(err != nil) "", "", err

Or just let fmt format the one liner return-only functions on one line instead of three:

err := syscall.Chdir(dir)
if err != nil { return &PathError{"chdir", dir, e} }

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil { return "", "", err }

It occurs to me that I can get everything I want with just the addition of some operator that returns early if the right-most error is not nil, if I combine it with named parameters:

func NewClient(...) (c *Client, err error) {
    defer annotateError(&err, "client couldn't be created")

    listener := net.Listen("tcp4", listenAddr)?
    defer closeOnErr(&err, listener)
    conn := ConnectionManager{}.connect(server, tlsConfig)?
    defer closeOnErr(&err, conn)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
         forwardOut = forwarding.NewOut(pop url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort)))
    }

    client := &Client{listener: listener, conn: conn, forward: forwardOut}

    toServer := communicationProtocol.Wrap(conn)
    toServer.Send(&client.serverConfig)?
    toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})?
    session := communicationProtocol.FinalProtocol(conn)?
    client.session = session

    return client, nil
}

func closeOnErr(err *error, c io.Closer) {
    if *err != nil {
        closeErr := c.Close()
        if err != nil {
            *err = multierror.Append(*err, closeErr)
        }
    }
}

func annotateError(err *error, annotation string) {
    if *err != nil {
        log.Printf("%s: %v", annotation, *err)
        *err = errwrap.Wrapf(annotation +": {{err}}", err)
    }
}

Currently my understanding of the Go consensus is against named parameters, but if the affordances of name parameters change, so can the consensus. Of course if the consensus is sufficiently strong against it, there's an option of including accessors.

This approach gets me what I'm looking for (making it easier to handle errors systematically at the top of a function, ability to factor such code, also line count reduction) with pretty much any of the other proposals here too, including the original. And even if the Go community decides it doesn't like it, I don't have to care, because it's in my code on a function-by-function basis and has no impedance mismatch in either direction.

Though I would express a preference for a proposal that permits a function of signature func GetInt() (x int, err error) to be used in code with OtherFunc(GetInt()?, "...") (or whatever the eventual result is) to one that can't be composed into an expression. While a lesser annoyance to the constant repetitious simple error handling clause, the amount of my code that unpacks a function of arity 2 just so it can have the first result is still annoyingly substantial, and doesn't really add anything to the clarity of the resulting code.

@thejerf, I feel like there's a lot of strange behavior here. You're calling net.Listen, which returns an error, but it's not assigned. And then you defer, passing in err. Does each new defer override the last one, so that annotateError is never invoked? Or do they stack, so that if an error is returned from, say, toServer.Send, then closeOnErr is called twice, and then annotateError is called? Is closeOnErr called only if the preceeding call has a matching signature? What about this case?

conn := ConnectionManager{}.connect(server, tlsConfig)?
fmt.Printf("Attempted to connect to server %#v", server)
defer closeOnErr(&err, conn)

Reading over the code, it also confuses things, like why can't I just say

client.session = communicationProtocol.FinalProtocol(conn)?

Presumably, because FinalProtocol is returning an err? But that's hidden from the reader.

Finally, what happens when I want to report an error and recover from it within a function? It seems like your example would prevent that case?

_Addendum_

OK, I think that when you want to recover from an error, by your example, you assign it, as in this line:

env, err := environment.GetRuntimeEnvironment()

That's fine because err is shadowed, but then if I changed...

forwardPort, err = env.PortToForward()
if err != nil {
    log.Printf("env couldn't provide forward port: %v", err)
}

to just

forwardPort = env.PortToForward()

Then your deferred error handled will not catch it, because you're using the err created in-scope. Or am I missing something?

I believe an addition to the syntax that denotes a function can fail is a good start. I propose something along these lines:

func (r Reader) Read(b []byte) (n int) fails {
    if somethingFailed {
        fail errors.New("something failed")
    }

    return 0
}

If a function fails (by using the fail keyword instead of return, it returns the zero value for each return parameter.

func (c EmailClient) SendEmail(to, content string) fails {
    if !c.connected() {
        fail errors.New("could not connect")
    }

    // You can handle it and execution will continue if you don't fail or return
    n := r.Read(b) handle (err) {
        fmt.Printf("failed to read: %s", err)
    }

    // This shouldn't compile and should complain about an unhandled error
    n := r.Read(b)
}

This approach will have the benefit of allowing current code to work, while enabling a new mechanism for better error handling (at least in syntax).

Comments on the keywords:

  • fails might not be the best option but it's the best I can think of currently. I thought of using err (or errs), but how they are used currently might make that a bad choice due to current expectations (err is most likely a variable name, and errs might be assumed to be a slice or array or errors).
  • handle could be misleading a bit. I wanted to use recover, but it's used for panics...

edit: Changed r.Read invocation to match io.Reader.Read().

Part of the reason for that suggestion is because the current approach in Go doesn't help tools to understand if a returned error value denotes a function failing or it's returning an error value as part of its function (e.g. github.com/pkg/errors).

I believe enabling functions to express failure explicitly is the first step to improving error handling.

@ibrasho , how is you example different from ...

func (c EmailClient) SendEmail(to, content string) error {
    // ...

    // You can handle it and execution will continue if you don't fail or return
    _, _, err := r.Read()
        if err != nil {
        fmt.Printf("failed to read: %s", err)
    }

    // This shouldn't compile and should complain about an unhandled error
    _, _, err := r.Read()
}

... if we give compiler warnings or lint for unhandled instances of error? No language change required.

Two things:

  • I think the proposed syntax reads and looks better. 😁
  • My version requires giving functions the ability to state they fail explicitly. This is something missing in Go currently that could enable tools to do more. We can always treat a function returning an error value as failing, but that's an assumption. What if the function returns 2 error values?

My suggestion had something that I removed, which was automatic propagation:

func (c EmailClient) SendEmail(to, content string) fails {
    n := r.Read(b)

    // Would automaticaly propgate the error, so it will be equivlent to this:
    // n := r.Read(b) handle (err) {
    //  fail err
    // }
}

I removed that since I think error handling should be explicit.

edit: Changed r.Read invocation to match io.Reader.Read().

So, this would be a valid signature or prototype?

func (r *MyFileReader) Read(b []byte) (n int, err error) fails

(Given that an io.Reader implementation assigns io.EOF when there's nothing more to read and some other error for failure conditions.)

Yes. But no one should expect err to denote a failure in the function doing its job. The Read failure error should be passed to the handle block. Errors are values in Go, and the one returned by this function should have no special meaning beside being something this function returns (for some odd reason).

I was proposing that a failure will result in return values being returned as zero-values. The current Reader.Read already make some promises that mightn't be possible with this new approach.

When Read encounters an error or end-of-file condition after successfully reading n > 0 bytes, it returns the number of bytes read. It may return the (non-nil) error from the same call or return the error (and n == 0) from a subsequent call. An instance of this general case is that a Reader returning a non-zero number of bytes at the end of the input stream may return either err == EOF or err == nil. The next Read should return 0, EOF.

Callers should always process the n > 0 bytes returned before considering the error err. Doing so correctly handles I/O errors that happen after reading some bytes and also both of the allowed EOF behaviors.

Implementations of Read are discouraged from returning a zero byte count with a nil error, except when len(p) == 0. Callers should treat a return of 0 and nil as indicating that nothing happened; in particular it does not indicate EOF.

Not all of this behavior is possible in the currently proposed approach. From the current Read interface contract, I see some shortcomings, like how to handle partial reads.

In general, how should a function behave when it's partially done by the time it fails? I honestly didn't think about this yet.

The case of io.EOF is simple:

func DoSomething(r io.Reader) fails {
    // I'm using rerr so that I don't shadow the err returned from the function
    n, err := r.Read(b) handle (rerr) {
        if rerr != io.EOF {
            fail err
        }
        // Else do nothing?
    }
}

@thejerf, I feel like there's a lot of strange behavior here. You're calling net.Listen, which returns an error, but it's not assigned.

I'm using the common ? operator proposed by multiple people to indicate returning the error with zero values for the other values, if the error is not nil. I slightly prefer a short word to an operator as I don't think Go is an operator-heavy language, but if ? was to go into the language I'd still count my winnings rather than stamp my feet in a huff.

Does each new defer override the last one, so that annotateError is never invoked? Or do they stack, so that if an error is returned from, say, toServer.Send, then closeOnErr is called twice, and then annotateError is called?

It works as defer does now: https://play.golang.org/p/F0xgP4h5Vxf I expected some downthumbs for that post, to which my planned reply was going to be to point out this is _already_ how defer works and you'd just be downthumbing the current Go behavior, but it didn't get any. Alas. As that snippet also shows, shadowing isn't an issue, or at least, isn't any more of an issue than it already is. (This would neither fix it, nor make it particularly worse.)

I think one aspect that may be confusing is that it is already the case in current Go that a named parameter is going to end up being "whatever is actually returned", so you can do what I did and take a pointer to it and pass that to a deferred function and manipulate it, regardless of whether you directly return a value like return errors.New(...), which might intuitively seem like a "new variable" that isn't the named variable, but in fact Go will end up with it assigned to the named variable by the time the defer runs. It's easy to ignore this particular detail of current Go right now. I submit that while it may be confusing now, if you worked even on a codebase that used this idiom (i.e., I'm not saying that this even has to become "Go best practice", just a few exposures would do) you'd figure it out pretty quickly. Because, just to say it one more time to be very clear, that's how Go already works, not a proposed change.

Here's a proposal that I think hasn't been suggested before. Using an example:

 r, !handleError := something()

The meaning of this is the same as this:

 r, _xyzzy := something()
 if ok, R := handleError(_xyzzy); !ok { return R }

(where _xyzzy is a fresh variable whose scope extends only to these two lines of code, and R may be multiple values).

Advantages to this proposal is not specific to errors, does not treat zero values specially, and it's easy to concisely specify how to wrap errors inside any particular block of code. The syntax changes are small. Given the straightforward translation, it's easy to understand how this feature works.

Disadvantages are that it introduces an implicit return, one can't write a generic handler that just returns the error (since its return values need to be based on the function where it's called from), and that the value that's passed to the handler is not available in the calling code.

Here's how you might use it:

func Read(filename string) error {
  herr := func(err error) (bool, error) {
      if err != nil { return true, fmt.Errorf("Read failed: %s", err) }
      return false, nil
  }

  f, !herr := OpenFile(filename)
  b, !herr := ReadBytes(f)
  !herr := ProcessBytes(b)
  return nil
}

Or you might use it in a test to call t.Fatal if your init code fails:

func TestSomething(t *testing.T) {
  must := func(err error) bool { t.Fatalf("init code failed: %s", err); return true }
  !must := setupTest()
  !must := clearDatabase()
  ...
}

I would suggest changing the function signature to just func(error) error. It simplifies the majority case, and if you need to further analyze the error, just use the current mechanism.

Syntax question: Can you define the function inline?

func Read(filename string) error {
    f, !func(err error) error {
        if err != nil { return true, fmt.Errorf("... %s", err) }
        return false, nil
    } := OpenFile(filename)
    /...

I'm comfortable with "don't do that", but the syntax should probably permit it to reduce special case count. Such would also permit:

func failed(s string) func(error) error {
    return func(err error) {
       // returns a decorated error with the given string
   }
}

func Read(filename string) error {
  f, !failed("couldn't open file") := OpenFile(filename)
  b, !failed("couldn't read file") := ReadBytes(f)
  !failed("couldn't process file") := ProcessBytes(b)
  return nil
}

Which strikes me as one of the better proposals for that sort of thing, at least in terms of getting that stuff in there concisely. This is another case where IMHO you're giving off better errors at this point than exception-based code tends to, which often fails to get this detailed about the nature of the error because it's so easy to just let exceptions propagate.

I'd also suggest for performance reasons that bang error functions be defined as not being called on zero values. That keeps their performance impact fairly minimal; in the case I just showed, if Read succeeds normally, it's no more expensive than a current read implementation that already is ifing on every error and failing the if clause. If we're calling a function on nil all the time, that's going to become very expensive whenever it can't be inlined, which is going to end up being a non-trivial amount of the time. (If an error is actively occurring, we can probably justify & afford a function call under almost any circumstance (if you can't then fall back to the current method), but don't really want that for non-errors.) It also means bang functions can assume a non-zero value in their implementation, which simplifies them too.

@thejerf nice but happy path is heavily altered.
many messages ago there were suggestion to have kinda Ruby like "or" sintax - f := OpenFile(filename) or failed("couldn't open file").

Additional concern - is that for any type of params or for errors only? if for errors only - then type error must have special meaning to compiler.

@thejerf nice but happy path is heavily altered.

I would recommend distinguishing between the original proposal's probably-common-path where it looks like paulhankin's original suggestion:

func Read(filename string) error {
  herr := func(err error) (bool, error) {
      if err != nil { return true, fmt.Errorf("Read failed: %s", err) }
      return false, nil
  }

  f, !herr := OpenFile(filename)
  b, !herr := ReadBytes(f)
  !herr := ProcessBytes(b)
  return nil
}

perhaps even with herr factored out somewhere, and my explorations of what it would take to spec it out completely, which is a necessity for this conversation, and my own musings on how I might use it in my personal code, which is merely an exploration of what else is enabled and afforded by a suggestion. I already said literally inlining a function is probably a bad idea after all, but the grammar should probably permit it to keep the grammar simple. I can already write a Go function that takes three functions, and inline all of them right into the call. That doesn't mean Go is broken or that Go needs to do something to prevent that; it means I shouldn't do that if I value code clarity. I like that Go affords code clarity, but there's still some degree of responsibility irreducibly on the developers to keep code clear.

If you're going to tell me that the "happy path" for

func Read(filename string) error {
  f, !failed("couldn't open file") := OpenFile(filename)
  b, !failed("couldn't read file") := ReadBytes(f)
  !failed("couldn't process file") := ProcessBytes(b)
  return nil
}

is crufted up and hard to read, but the happy path is easy to read with

func Read(filename string) error {
  f, err := OpenFile(filename)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  b, err := ReadBytes(f)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  err = ProcessBytes(b)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  return nil
}

then I submit the only possible way in which the second is easier to read is that you're used to reading it already and your eyes are trained to skip down exactly the right path to see the happy path. I can guess that, because mine are too. But once you're used to an alternate syntax, you'll be trained for that too. You should analyze the syntax based on how you'll feel when you're already used to it, not how you feel now.

I'd also note that the added newlines in the second example represents what happens to my real code. It's not just "lines" of code that current Go error handling tends to add to code, it also adds a lot of "paragraphs" to what otherwise should be a very simple function. I want to open the file, read some bytes, and process them. I don't want to

Open a file.

And then read some bytes, if that's OK.

And then process them, if that's OK.

I feel like there's a lot of downvotes that amount to "this isn't what I'm used to", rather than an actual analysis of how these will play out in real code, once you're used to them and using them fluently.

I don't like the idea of hiding return statement, I prefer:

f := OpenFile(filename) or return failed("couldn't open file")
....
func failed(msg string, err error) error { ... } 

In this case or is a zero conditional forwarding operator,
forwarding the last return if it's non-zero.
There is a similar proposal in C# using the operator ?>

f := OpenFile(filename) ?> return failed("couldn't open file")

@thejerf "happy path" in your case prepended by call to failed(...), which might be very long. also sound as yoda :rofl:

@rodcorsi characters entered with "shift" key are bad IMHO - (if you have multiple keyboard layouts)

Please don't make this way more complex than it is now. Really moving the same codes in one line(instead of 3 or more) is not really a solution. I personally don't see any of these proposals viable that much. Guys, the math is very simple. Either embrace the "Try-catch" idea or keep the things the way they are now which means lots of "if then else"s and code noises and is not really suitable for using in OO patterns like Fluent Interface.

Thank you very much for all your hand-downs and perhaps few hand-ups ;-) (just kidding)

@KamyarM IMO, "use the best known alternative or make no changes at all" is not a very productive statement. It halts innovation and facilitates circular arguments.

@KernelDeimos I agree with you but I see many comments on this thread which was essentially advocating the old with moving exact 4 5 lines into one single line which I don't see it as really solution and also many in Go Community very RELIGIOUSLY reject using Try-Catch which close the doors for any other opinions. I personally think those who invented this try-catch concept really thought about it and although it might have few flaws but those flaws are just caused by bad programming habits and there is no way to force programmers to write good codes even if you remove or limit all the good or some may say bad features a programming language may have.
I proposed something like this before and this is not exactly java or C# try-catch and I think it can support current error handling and libraries and I use one of the examples from above. So basically the compiler checks for the errors after each line and jump to the catch block if err value is set to not nil:

try (var err error){ 
    f, err := OpenFile(filename)
    b, err := ReadBytes(f)
    err = ProcessBytes(b)
    return nil
} catch (err error){ //Required
   return err
} finally{ // Optional
    // Do something else like close the file or connection to DB. Not necessary in this example since we  return earlier.
}

@KamyarM
In your example, how do I know (at the time of writing the code) which method returned the error? How do I fulfill the third way of handling the error ("return the error with additional contextual information")?

@urandom
one way is to use the Go switch and find the type of exception in the catch. Lets say you have pathError exception you know that is caused by OpenFile() that way. Another way that is not much different from the current if err!=nil error handling in GoLang is this:

try (var err error){ 
    f, err := OpenFile(filename)
} catch (err error){ //Required
   return err
}
try (var err error){ 
    b, err := ReadBytes(f)
catch (err error){ //Required
   return err
}
try (var err error){ 
    err = ProcessBytes(b)
catch (err error){ //Required
   return err
}
return nil

So you have options this way but you are not limited. If you really want to know exactly which line caused issue you put every single line in a try catch the same way you right now write lots of if-then-elses. If the error is not important for you and you want to pass it to the caller method which in the examples discussed in this thread are actually about that I think my proposed code just does the job.

@KamyarM I see where you're coming from now. I would say if there are so many people against try...catch, I see that as evidence that try...catch isn't perfect and has faults. It's an easy solution to a problem, but if Go2 can make error handling better than what we've seen in other languages I think that would be really cool.

I think it's possible to take what's good about try...catch without taking what's bad about try...catch, which I proposed earlier. I agree that turning three lines into 1 or 2 solves nothing.

The fundamental problem, as I see it, is the error-handling code in a function gets repeated if part of the logic is "return to the caller". If you want to change the logic at any point, you have to change every instance of if err != nil { return nil }.

That said, I do really like the idea of try...catch as long as functions can't throw anything implicitly.

Another thing I think would be useful is if the logic in catch {} required a break keyword to break the control flow. Sometimes you want to handle an error without breaking the control flow. (ex: "for each item of these data do something, add a non-nil error to a list and continue")

@KernelDeimos I complete agree with that. I have seen the exact situation like that. You need to capture errors as much as you can before breaking the codes. If something like Channels could be used on those situations in GoLang that was good.then you could send all the errors to that channel the catch is expecting and then catch could handle them one by one.

I'd rather blend "or return" with #19642, #21498 than use try..catch (defer/panic/recover already exists; throwing within same function is like have multiple goto statements and became messy with additional type switch inside catch; allows forget error handling by having try..catch high up in the stack (or significantly complicate compiler if scope try..catch inside single function)

@egorse
It seems the try-catch syntax @KamyarM is suggesting is some syntax sugar for handling error return variables, not an introduction for exceptions. While I prefer an "or return" type syntax for various reasons, it does seem like a legit suggestion.

That being said, @KamyarM , why does the try have a variable definition portion in it? You are defining an err variable, but its being shadowed by the other err variables within the block itself. What is its purpose?

I think it's to tell it what variable to keep an eye on, allowing it to be decoupled from the error type. It probably would require a change to shadowing rules, unless you just needed people to be really careful with it. I'm not sure about the declaration in the catch block, though.

@egorse Exactly what @DeedleFake mentioned there is the purpose of it. It means that try block has an eye on that object. Also it limits its scope. It is something similar to the using statement in C#. In C# the object(s) that are defined by using keyword automatically are disposed once that block is executed and the scope of those object(s) are limited to the "Using" block.
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement

Using the catch is necessary because we want to force the programmer to decide how to handle the error right a way. In C# and Java catch is also mandatory. in C# if you don't want to handle an exception you don't use try-catch in that function at all. When an exception happens any method in the call hierarchy can handle the exception or even re-throw(or wrap it in another exception) it again. I don't think you can do the same thing in Java. In Java a method that may throw an exception needs to declare it in the function signature.

I want to emphasize that this try-catch block is not the exact one. I used these keywords because this is similar in what it wants to achieve and also this is what many programmers are familiar with and are taught in most programming concept courses.

There could be a _return on error_ assignment, which works only if there is a _named error return parameter_, as in:

func process(someInput string) (someOutput string, err error) {
    err ?= otherAction()
    return
}

If err is not nil then return.

I think this discussion of adding a try sugar to Rust would be illuminating to participants in this discussion.

FWIW, an old thought on simplifying error handling (apologies if this is nonsense):

The raise identifier, denoted by the caret symbol ^, may be used as one of the operands on the left hand side of an assignment. For the purposes of the assignment, the raise identifier is an alias for the last return value of the containing function, whether or not the value has a name. After the assignment completes, the function tests the last return value against its type's zero value (nil, 0, false, ""). If it is considered zero, the function continues to execute, otherwise it returns.

The primary purpose of the raise identifier is to concisely propagate errors from called functions back to the caller in a given context without hiding the fact that this is occurring.

As an example, consider the following code:

func Alpha() (string, error) {

    b, ^ := beta()
    g, ^ := gamma()
    return b + g, nil
}

This is roughly equivalent to:

func Alpha() (ret1 string, ret2 error) {

    b, ret2 := beta()
    if ret2 != nil {
        return
    }

    g, ret2 := gamma()
    if ret2 != nil {
        return
    }

    return b + g, nil
}

The program is malformed if:

  • the raise identifier is used more than once in an assignment
  • the function does not return a value
  • the type of the last return value does not have a meaningful and efficient test for zero

This suggestion is similar to others in that it does not address the problem of providing greater contextual information, for what that's worth.

@gboyle That's why IMO last return value should be named and of type error. This has two important implications:

1 - other return values are named too, hence
2 - they already have meaningful zero values.

@object88 As the history of context package shows us, this needs some action from the core team, like defining a built-in error type (just a normal Go error) with some common attributes (message? call stack? etc etc).

AFAIK there are not much contextual language constructs in Go. Besides go and defer there are no other ones and even these two are very explicit and clear (bot in syntax - and to the eye - and semantics).

What about something like this?

(copied some real code I'm working on):

func (g *Generator) GenerateDevices(w io.Writer) error {
    var err error
    catch err {
        _, err = io.WriteString(w, "package cc\n\nconst (") // if err != nil { goto Caught }
        for _, bd := range g.zwClasses.BasicDevices {
            _, err = w.Write([]byte{'\t'}) // if err != nil { goto Caught }
            _, err = io.WriteString(w, toGoName(bd.Name)) // if err != nil { goto Caught }
            _, err = io.WriteString(w, " BasicDeviceType = ") // if err != nil { goto Caught }
            _, err = io.WriteString(w, bd.Key) // if err != nil { goto Caught }
            _, err = w.Write([]byte{'\n'}) // if err != nil { goto Caught }
        }
        _, err = io.WriteString(w, ")\n\nvar BasicDeviceTypeNames = map[BasicDeviceType]string{\n") // if err != nil { goto Caught }
       // ...snip
    }
    // Caught:
    return err
}

When err is non-nil it breaks to the end of the "catch" statement. You can use "catch" to group similar calls together that typically return the same type of error. Even if the calls were not related you could check the error types afterwards and wrap it appropriately.

@lukescott have a read of this blog post by @robpike https://blog.golang.org/errors-are-values

@davecheney The catch idea (without the try) keeps in the spirit of that sentiment. It treats error as a value. It simply breaks (within the same function) when the value is no longer nil. It doesn't crash the program in any way.

@lukescott you can use Rob's technique today, you don't need to change the language.

There is a rather big difference between exceptions and errors:

  • errors are expected (we can write test for them),
  • exceptions are not expected (hence the "exception"),

Many languages treat both as exceptions.

Between generics and better error handling, I would choose better error handling since most code clutter in Go comes from error handling. While it can be said this kind of verbosity is good and is in favor of simplicity, IMO it also obscures the _happy path_ of a workflow to the level of being ambiguous.

I'd like to build on the proposal from @thejerf just a little bit.

First, instead of a !, an or operator is introduced, that shift's the last returned argument from the function call on the left side, and invokes a return statement on the right, whose expression is a function that is called, if the shifted argument is non-zero (non-nill for error types), passing it that argument. Its fine if people think should be only for error types as well, though I feel that this construct will be useful for functions that return a boolean as their last argument as well (is something ok/not ok).

The Read method will look like so:

func Read(filename string) error {
  f := OpenFile(filename) or return errors.Contextf("opening file %s", filename)
  b := ReadBytes(f) or return errors.Contextf("reading file %s", filename)
  ProcessBytes(b) or return errors.Context("processing data")
  return nil
}

I'm assuming the errors package provides convenience functions like the following:

func Noop() func(error) error {
   return func(err error) {
       return err   
   }
}


func Context(msg string) func(error) error {
    return func(err error) {
        return fmt.Errorf("%s: %v", msg, err)
    }
}
...

This looks perfectly readable, while covering all necessary points, and it doesn't look too foreign as well, due to the familiarity of the return statement.

@urandom In this statement f := OpenFile(filename) or return errors.Contextf("opening file %s", filename) how the reason can be known? For example is it the lack of read permission or the file does not exists at all?

@dc0d
Well, even in the above example, the original error is included, as the user given message is just added context. As stated, and derived from the original proposal, or return expects a function that receives a single parameter of the shifted type. This is key, and allows for not only utility functions that will suite quite a large crowd, but you can pretty much write your own if you need a really custom handling of specific values.

@urandom IMO it hides too much.

My 2 cents here, I would like to propose a simple rule:

"implicit result error parameter for functions"

For any function, an error parameter is implied at the end of the result parameter list
if not explicitly defined.

Assume we have a function defined as follows for the sake of discussion:

func f() (int) {}
which is identical to: func f() (int, error) {}
according to our implicit result error rule.

for assignment, you can bubble up, ignore, or catch the error as follows:

1) bubble up

x := f()

if f returns error, the current function will immediately return with the error
(or create a new error stack?)
if the current function is main, the program will halt.

It is equivalent to the following code snippet:

x, err := f()
if err != nil {
return ..., err
}

2) ignore

x, _ := f()

a blank identifier at the end of the assignment expression list to explicitly signal discarding of the error.

3) catch

x, err := f()

err must be handled as usual.

I believe this idiomatic code convention change should only require minimal changes in the compiler
or a preprocessor should do the job.

@dc0d Can you give an example of what it hides, and how?

@urandom It is the reason that caused the question "where is the original error?", as I asked in a previous comment. It passes the error implicitly and it is not clear (for example) where is the original error placed in this line: f := OpenFile(filename) or return errors.Contextf("opening file %s", filename). The original error returned by OpenFile() - which might be something like lack of read permission or file being absent and not just "there is something wrong with filename".

@dc0d
I disagree. It's about as clear as dealing with http.Handlers, where with the later you pass them to some mux, and suddenly you get a request and a response writer. And people are used to this type of behavior. How do people know what the go statement does? Its obviously not clear on the first encounter, yet is quite pervasive and is in the language.

I don't think we should be against any proposal on the grounds that its new and no one has any idea how it works, because that's true for most of them.

@urandom Now that makes sense a bit more (including http.Handler example).

And we are discussing things. I do not speak against or for any specific idea. But I do support simplicity and being explicit and at the same time conveying a bit of sanity about developer experience.

@dc0d

which might be something like lack of read permission or file being absent

In that case you wouldn't just re-throw the error but check it actual contents. For me this issue is about covering the most popular case. That is, re-throwing error with added context. Only in more rare cases do you convert error to some concrete type and check what it actually says. And for that current error handling syntax is perfectly fine and will not go anywhere even if one of the proposals here would get accepted.

@creker Errors are not exceptions (some previous comment of mine). Errors are values so throwing or re-throwing them is not possible. For try/catch-like scenarios Go has panic/recover.

@dc0d I'm not talking about exceptions. By re-throwing I mean returning error to the caller. The proposed or return errors.Contextf("opening file %s", filename) basically wraps and re-throws an error.

@creker Thanks for the explanation. It also adds some extra function calls which affects the scheduler which in turn might not produce the desired behavior in some situations.

@dc0d that's an implementation detail and might change in the future. And it could actually change, non-cooperative preemption is in the works right now.

@creker
I think you can cover even more cases than just returning a modified error:

func retryReadErrHandler(filename string, count int) func(error) error {
     return func(err error) error {
          if os.IsTimeout(err) {
               count++
               return Read(filename, count)
          }
          if os.IsPermission(err) {
               log.Fatal("Permission")
          }

          return fmt.Errorf("opening file %s: %v", filename, err)
      }
}

func Read(filename string, count int) error {
  if count > 3 {
    return errors.New("max retries")
  }

  f := OpenFile(filename) or return retryReadErrHandler(filename, count)

  ...
}

@dc0d
The extra function calls will probably be inlined by the compiler

@urandom that looks very interesting. A bit magical with implicit argument but this one could actually be general and concise enough to cover everything. Only in very rare cases would you have to resort to regular if err != nil

@urandom, I'm confused by your example. Why is retryReadErrHandler returning a func?

@object88
That's the idea behind the or return operator. It expects a function which it will call in the event of a non-zero last returned argument from the left side. In this regard it acts exactly the same as an http.Handler, leaving the actual logic of how to handle the argument and its return (or the request and its response, in the case of a handler), to the callback. And in order to use your own custom data in the callback, you create a wrapper function that receives that data as parameters and return what is expected.

Or in more familiar terms, it is similar to what we usually do with handlers:
```go
func nodesHandler(repo Repo) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
data, _ := json.Marshal(repo.GetNodes())
w.Write(data)
})
}

@urandom, you might avoid some magic by leaving the LHS the same as today and changing or ... return to returnif (cond):

func Read(filename string) error {
   f, err := OpenFile(filename) returnif(err != nil) errors.Contextf(err, "opening file %s", filename)
   b, err := ReadBytes(f) returnif(err != nil) errors.Contextf(err, "reading file %s", filename)
   err = ProcessBytes(b) returnif(err != nil) errors.Context(err, "processing data")
   return nil
}

This improves generality and transparency of the error values on the left and the triggering condition on the right.

The more I see these different proposals, the more I'm inclined to want a gofmt-only change. The language already has the power, let's just make it more scannable. @billyh, not to pick on your suggestion in particular but returnif(cond) ... is just a way of rewriting if cond { return ...}. Why can't we just write the latter? We already know what it means.

x, err := foo()
if err != nil { return fmt.Errorf(..., err) }

or even

if x, err := foo(); err != nil { return fmt.Errorf(..., err) }

or

x, err := foo(); if err != nil { return fmt.Errorf(..., err) }

No new magic keywords or syntax or operators.

(It might help if we also fix #377 to add some flexibility to the use of :=.)

@randall77 I'm increasingly inclined that way, too.

@randall77 At what point will that block be line wrapped?

The solution above is more agreeable when compared to the alternatives proposed here, but Im not convinced that its better than taking no action. Gofmt should be as deterministic as possible.

@as, I haven't totally thought it through, but maybe "if the body of an if statement contains a single return statement, then the if statement is formatted as a single line."

Maybe there needs to be an additional restriction on the if condition, like it must be a boolean variable or a binary operator of two variables or constants.

@billyh
I don't see the need to make it more verbose, as I don't see anything confusing from that little bit of magic in the or. I assume that unlike @as , a lot of people don't find anything confusing in the way we work with http handlers either.

@randall77
What you suggest is more in line as a code style suggestion, and that's where go is highly opinionated. It might not play well in the community as a whole for there suddenly to be 2 styles of formatting if statements.

Not to mention that one liners like those are much harder to read. if != ; { } is too much even in several lines, hence this proposal. The pattern is fixed for almost all cases and can be turned into syntactic sugar that easy to read and understand.

The problem I have with most of these suggestions is it's not clear what's going on. In the opening post it is suggested to re-use || for returning an error. It isn't clear to me that a return is happening there. I think that if new syntax is going to be invented, it needs to align with most people's expectations. When I see || I do not expect a return, or even a break in execution. It's jarring to me.

I do like Go's "errors are values" sentiment, but I also agree that if err := expression; err != nil { return err } is too verbose, mainly because almost every call is expected to return an error. This means you're going to have a lot of these, and it's easy to mess it up, depending on where err is declared (or shadowed). It has happened with our code.

Since Go does not use try/catch and uses panic/defer for "exceptional" circumstances, we may have an opportunity to reuse the try and/or catch keywords for shortening error handling without crashing the program.

Here is a thought I had:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, try err = io.WriteString(w, "bar")
    return
}

The thought is you prefix err on the LHS with the keyword try. If err is non-nil a return happens immediately. You do not have to use a catch here, unless the return isn't completely satisfied. This aligns more with people's expectations of "try breaks execution", but instead of crashing the program it just returns.

If the return is not completely satisfied (compile time check) or we want to wrap the error, we could use catch as special forward-only label like this:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, try err = io.WriteString(w, "bar")
    return
catch:
    return &CustomError{"some detail", err}
}

This also allows you to check and ignore certain errors:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, err = io.WriteString(w, "bar")
        if err == io.EOF {
            err = nil
        } else {
            goto catch
        }
    return
catch:
    return &CustomError{"some detail", err}
}

Perhaps you could even have try specify the label:

func WriteFooBar(w io.Writer) (err error) {
    _, try(handle1) err = io.WriteString(w, "foo")
    _, try(handle2) err = w.Write([]byte{','})
    _, try(handle3) err = io.WriteString(w, "bar")
    return
handle1:
    return &CustomError1{"...", err}
handle2:
    return &CustomError2{"...", err}
handle3:
    return &CustomError3{"...", err}
}

I realize my code examples kind of suck (foo/bar, ack). But hopefully I've illustrated what I mean by going with/against existing expectations. I would also be totally fine with keeping errors the way they are in Go 1. But if a new syntax is invented, there needs to be careful thought about how that syntax is already perceived, not just in Go. It's hard to invent a new syntax without it already meaning something, so it's often better to go with existing expectations rather than against them.

Maybe some sort of chaining like how you can chain methods but for errors? I'm not really sure what that would look like or if it would even work, just a wild idea.
You can sort of chain now to reduce the number of error checks by maintaining some sort of error value within a struct and extracting it at the end of the chain.

It's a very curious situation because while there's a bit of boilerplate, I'm not quite sure how to simplify it further while still making sense.

@thejerf's sample code looks like this with @lukescott's proposal:

func NewClient(...) (*Client, error) {
    listener, try err := net.Listen("tcp4", listenAddr)
    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, try err := ConnectionManager{}.connect(server, tlsConfig)
    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil

catch:
    return nil, err
}

It goes from 59 lines down to 47.

This is the same length, but I think it's a little more clear than using defer:

func NewClient(...) (*Client, error) {
    var openedListener, openedConn bool
    listener, try err := net.Listen("tcp4", listenAddr)
    openedListener = true

    conn, try err := ConnectionManager{}.connect(server, tlsConfig)
    openedConn = true

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil

catch:
    if openedConn {
        conn.Close()
    }
    if openedListener {
        listener.Close()
    }
    return nil, err
}

That example would probably be easier to follow with a deferifnotnil or something.
But that kinda goes back to the whole one line if thing which many of these suggestions are about.

Having played with the example code a little, I'm now against the try(label) name variant. I think if you have multiple fancy things to do, just use the current system of if err != nil { ... }. If you're doing basically the same thing, such as setting a custom error message, you could do this:

func WriteFooBar(w io.Writer) (err error) {
    msg := "thing1 went wrong"
    _, try err = io.WriteString(w, "foo")
    msg = "thing2 went wrong"
    _, try err = w.Write([]byte{','})
    msg = "thing3 went wrong"
    _, try err = io.WriteString(w, "bar")
    return nil

catch:
    return &CustomError{msg, err}
}

If anyone has used Ruby, this looks a lot like their rescue syntax, which I think reads reasonably well.

One thing which could be done is to make nil a falsy value and for other values to evaluate to true, so you wind up with:

err := doSomething()
if err { return err }

But I'm not sure that would really work and it only shaves off a few characters.
I've typoed many things, but I don't think I've ever typoed != nil.

Making interfaces truthy/falsey has been mentioned before, and I said that it would make bugs with flags more common:

verbose := flag.Bool("v", false, "verbose logging")
flag.Parse()
if verbose { ... } // should be *verbose!

@carlmjohnson , in the example you provided just above, there are error messages interspersed with happy-path code, which is a little strange to me. If you need to format those strings, then you're doing a lot of extra work regardless of whether anything goes wrong, or not:

func (f *foo) WriteFooBar(w io.Writer) (err error) {
    msg := fmt.Sprintf("While writing %s, thing1 went wrong", f.foo)
    _, try err = io.WriteString(w, f.foo)
    msg = fmt.Sprintf("While writing %s, thing2 went wrong", f.separator)
    _, try err = w.Write(f.separator)
    msg = fmt.Sprintf("While writing %s, thing3 went wrong", f.bar)
    _, try err = io.WriteString(w, f.bar)
    return nil

catch:
    return &CustomError{msg, err}
}

@object88 , I think that the SSA analysis should be able to figure out if certain assignments aren't used and rearrange them to not happen if not needed (too optimistic?). If that's true, this should be efficient:

func (f *foo) WriteFooBar(w io.Writer) (err error) {
    var format string, args []interface{}

    msg = "While writing %s, thing1 went wrong", 
    args = []interface{f.foo}
    _, try err = io.WriteString(w, f.foo)

    format = "While writing %s, thing2 went wrong"
    args = []interface{f.separator}
    _, try err = w.Write(f.separator)

    format = "While writing %s, thing3 went wrong"
    args = []interface{f.bar}
    _, try err = io.WriteString(w, f.bar)
    return nil

catch:
    msg := fmt.Sprintf(format, args...)
    return &CustomError{msg, err}
}

Would this be legal?

func Foo() error {
catch:
    try _ = doThing()
    return nil
}

I think that should loop until doThing() returns nil, but I could be convinced otherwise.

@carlmjohnson

Having played with the example code a little, I'm now against the try(label) name variant.

Yeah, I wasn't sure about the syntax. I don't like it because it makes try look like a function call. But I could see the value of specifying a different label.

Would this be legal?

I would say yes because try should be forward-only. If you wanted to do that, I would say you need to do it like this:

func Foo() error {
tryAgain:
    if err := doThing(); err != nil {
        goto tryAgain
    }
    return nil
}

Or like this:

func Foo() error {
    for doThing() != nil {}
    return nil
}

@Azareal

One thing which could be done is to make nil a falsy value and for other values to evaluate to true, so you wind up with: err := doSomething() if err { return err }

I think there is value in shortening it. However, I don't think it should apply to nil in all situations. Perhaps there could be a new interface like this:

interface Truthy {
  True() bool
}

Then any value that implements this interface can be used as you proposed.

This would work as long as the error implemented the interface:

err := doSomething()
if err { return err }

But this would not work:

err := doSomething()
if err == true { return err } // err is not true

I'm really new to golang but how do you think about introducing conditional delegator like below?

func someFunc() error {

    errorHandler := delegator(arg1 Arg1, err error) error if err != nil {
        // ...
        return err // or modifiedErr
    }

    ret, err := doSomething()
    delegate errorHandler(ret, err)

    ret, err := doAnotherThing()
    delegate errorHandler(ret, err)

    return nil
}

delegator is function like stuff but

  • Its return means return from its caller context. (return type must be same as caller)
  • It takes optionally if before {, in above example it is if err != nil.
  • It must be delegated by the caller with delegate keyword

It might be able to omit delegate to delegate, but I think it makes hard to read flow of the function.

And perhaps it is useful not only for error handling, I'm not sure now though.

It's beautiful to add check, but you can do further before returning:

result, err := openFile(f);
if err != nil {
        log.Println(..., err)
    return 0, err 
}

becomes

result, err := openFile(f);
check err

```Go
result, err := openFile(f);
check err {
log.Println(..., err)
}

```Go
reslt, _ := check openFile(f)
// If err is not nil direct return, does not execute the next step.

```Go
result, err := check openFile(f) {
log.Println(..., err)
}

It also attempts simplifying the error handling (#26712):
```Go
result, err := openFile(f);
check !err {
    // err is an interface with value nil or holds a nil pointer
    // it is unusable
    result.C...()
}

It also attempts simplifying the (by some considered tedious) error handling (#21161). It would become:

result, err := openFile(f);
check err {
   // handle error and return
    log.Println(..., err)
}

Of course, you can use a try and other keywords instead of the check , if it's clearer.

reslt, _ := try openFile(f)
// If err is not nil direct return, does not execute the next step.

```Go
result, err := openFile(f);
try err {
// handle error and return
log.Println(..., err)
}

Reference:

A plain idea, with support for error decoration, but requiring a more drastic language change (obviously not for go1.10) is the introduction of a new check keyword.

It would have two forms: check A and check A, B.

Both A and B need to be error. The second form would only be used when error-decorating; people that do not need or wish to decorate their errors will use the simpler form.

1st form (check A)
check A evaluates A. If nil, it does nothing. If not nil, check acts like a return {<zero>}*, A.

Examples

If a function just returns an error, it can be used inline with check, so
```Go
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

becomes

check UpdateDB()

For a function with multiple return values, you'll need to assign, like we do now.

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

becomes

a, b, err := Foo()
check err

// use a and b

2nd form (check A, B)
check A, B evaluates A. If nil, it does nothing. If not nil, check acts like a return {}*, B.

This is for error-decorating needs. We still check on A, but is B that is used in the implicit return.

Example

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

becomes

a, err := Foo()
check err, &BarError{"Bar", err}

Notes
It's a compilation error to

use the check statement on things that do not evaluate to error
use check in a function with return values not in the form { type }*, error
The two-expr form check A, B is short-circuited. B is not evaluated if A is nil.

Notes on practicality
There's support for decorating errors, but you pay for the clunkier check A, B syntax only when you actually need to decorate errors.

For the if err != nil { return nil, nil, err } boilerplate (which is very common) check err is as brief as it could be without sacrificing clarity (see note on the syntax below).

Notes on syntax
I'd argue that this kind of syntax (check .., at the beginning of the line, similar to a return) is a good way to eliminate error checking boilerplate without hiding the control flow disruption that implicit returns introduce.

A downside of ideas like the || and catch above, or the a, b = foo()? proposed in another thread, is that they hide the control flow modification in a way that makes the flow harder to follow; the former with || machinery appended at the end of an otherwise plain-looking line, the latter with a little symbol that can appear everywhere, including in the middle and at the end of a plain-looking line of code, possibly multiple times.

A check statement will always be top-level in the current block, having the same prominence of other statements that modify the control flow (for example, an early return).

Here's another thought.

Imagine an again statement which defines a macro with a label. The statement statement it labels can be expanded again by textual substitution later in the function (reminiscent of const/iota, with shades of goto :-] ).

For example:

func(foo int) (int, error) {
    err := f(foo)
again check:
    if err != nil {
        return 0, errors.Wrap(err)
    }
    err = g(foo)
    check
    x, err := h()
    check
    return x, nil
}

would be exactly equivalent to:

func(foo int) (int, error) {
    err := f(foo)
    if err != nil {
        return 0, errors.Wrap(err)
    }
    err = g(foo)
    if err != nil {
        return 0, errors.Wrap(err)
    }
    x, err := h()
    if err != nil {
        return 0, errors.Wrap(err)
    }
    return x, nil
}

Note that the macro expansion has no arguments - this means that there should be less confusion about the fact that it is a macro, because the compiler doesn't like symbols on their own.

Like the goto statement, the scope of the label is within the current function.

Interesting idea. I liked the catch label idea, but I don't think it was a good fit with Go scopes (with current Go, you can't goto a label with new variables defined in its scope). The again idea fixes that problem because the label comes before new scopes are introduced.

Here is the mega-example again:

func NewClient(...) (*Client, error) {
    var (
        err      error
        listener net.Listener
        conn     net.Conn
    )
    catch {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener, try err = net.Listen("tcp4", listenAddr)

    conn, try err = ConnectionManager{}.connect(server, tlsConfig)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

Here is a version closer to Rog's proposal (I don't like it as much):

func NewClient(...) (*Client, error) {
    var (
        err      error
        listener net.Listener
        conn     net.Conn
    )
again:
    if err != nil {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener, err = net.Listen("tcp4", listenAddr)
    check

    conn, err = ConnectionManager{}.connect(server, tlsConfig)
    check

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    check

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    check

    session, err := communicationProtocol.FinalProtocol(conn)
    check
    client.session = session

    return client, nil
}

@carlmjohnson For the record, that's not quite what I'm suggesting. The "check" identifier isn't special - it needs to be declared by putting it after the "again" keyword.

Also, I'd suggest that the above example doesn't illustrate its use very well - there's nothing in the again-labelled statement above that couldn't just as well be done in a defer statement. In the try/catch example, that code cannot (for example) wrap the error with information about the source location of the error return. It also won't work AFAICS if you add a "try" inside one of those if statements (for example to check the error returned by GetRuntimeEnvironment), because the "err" referred to by the catch statement is in a different scope from that declared inside the block.

I think my only issue with a check keyword is that all exits to a function should be a return (or at least have _some_ kind of "I'm gonna leave the function" connotation). We _might_ get become (for TCO), at least become has some kind of "We're becoming a different function"... but the word "check" really doesn't sound like it's going to be an exit for the function.

The exit point of a function is extremely important, and I'm not sure if check really has that "exit point" feel. Other than that, I really like the idea of what check does, it allows for much more compact error handling, but still allows to handle each error differently, or to wrap the error how you wish.

Can I add a suggestion also?
What about something like this:

func Open(filename string) os.File onerror (string, error) {
       f, e := os.Open(filename)
       if e != nil { 
              fail "some reason", e // instead of return keyword to go on the onerror 
       }
      return f
}

f := Open(somefile) onerror reason, e {
      log.Prinln(reason)
      // try to recover from error and reasign 'f' on success
      nf = os.Create(somefile) onerror err {
             panic(err)
      }
      return nf // return here must return whatever Open returns
}

The error assignment can have any form, even be something stupid like

f := Open(name) =: e

Or return a different set of values in case of error not just errors, And also a try catch block would be nice.

try {
    f := Open("file1") // can fail here
    defer f.Close()
    f1 := Open("file2") // can fail here
    defer f1.Close()
    // do something with the files
} onerror err {
     log.Println(err)
}

@cthackers I personally believe that it's very nice for errors in Go to not have special treatment. They are simply values, and I think they should stay that way.

Also, try-catch (and similar constructs) is just all around a bad construct that encourages bad practices. Every error should be handled separately, not handled by some "catch all" error handler.

https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md
this is too complicated.

my idea: |err| means check error : if err!=nil {}

// common util func
func parseInt(s string) (i int64, err error){
    return strconv.ParseInt(s, 10, 64)
}

// expression check err 1 : check and use err variable
func parseAndSum(a string ,b string) (int64,error) {
    sum := parseInt(a) + parseInt(b)  |err| return 0,err
    return sum,nil
} 

// expression check err 2 : unuse variable 
func parseAndSum(a string , b string) (int64,error) {
    a,err := parseInt(a) |_| return 0, fmt.Errorf("parseInt error: %s", a)
    b,err := parseInt(b) |_| { println(b); return 0,fmt.Errorf("parseInt error: %s", b);}
    return a+b,nil
} 

// block check err 
func parseAndSum(a string , b string) (  int64,  error) {
    {
      a := parseInt(a)  
      b := parseInt(b)  
      return a+b,nil
    }|err| return 0,err
} 

@chen56 and all future commenters: See https://go.googlesource.com/proposal/+/master/design/go2draft.md .

I suspect this obsoletes this thread now and there is little point in continuing on here. The Wiki feedback page is where things should probably go in the future.

The mega example using the Go 2 proposal:

func NewClient(...) (*Client, error) {
    var (
        listener net.Listener
        conn     net.Conn
    )
    handle err {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener = check net.Listen("tcp4", listenAddr)

    conn = check ConnectionManager{}.connect(server, tlsConfig)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    check toServer.Send(&client.serverConfig)

    check toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session := check communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

I think this is about as clean as we can hope for. The handle block has the good qualities of the again label or Ruby's rescue keyword. The only questions left in my mind are whether to use punctuation or a keyword (I think keyword) and whether to allow for getting the error without returning it.

I'm trying to understand the proposal - it appears that there is only one handle block per function, rather than the ability to create different responses to different errors throughout the function execution processes. This seems like a real weakness.

I'm also wondering if we are overlooking the critical need for developing testing harnesses in our systems as well. Considering how we are going to exercise error paths during tests should be part of the discussion, but I don't see that either,

@sdwarwick I don't think this is the best place to discuss the design draft described at https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling.md . A better approach is to add a link to a writeup at the wiki page at https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback .

That said, that design draft does permit multiple handle blocks in a function.

This issue started out as a specific proposal. We aren't going to adopt that proposal. There has been a lot of great discussion on this issue, and I hope that people will pull the good ideas out into separate proposals and into discussion on the recent design draft. I'm going to close out this issue. Thanks for all the discussion.

If to speak in the set of these examples:

r, err := os.Open(src)
    if err != nil {
        return err
    }

That I would like to write in one line approximately so:

r, err := os.Open(src) try ("blah-blah: %v", err)

Instead of "try", put any beautiful and suitable word.

With such a syntax, the error would return and the rest would be some default values depending on the type. If I need to return along with an error and something else specific, rather than default, then no one cancels the classic more multi-line option.

Even more shortly (without adding some kind of error handling):

r, err := os.Open(src) try

)
P.S. Excuse me for my English))

My variant:

func CopyFile(src, dst string) string, error {
    r := check os.Open(src) // return nil, error
    defer r.Close()

    // if error: run 1 defer and retun error message
    w := check os.Create(dst) // return nil, error
    defer w.Close()

    // if error: run 2, 1 defer and retun error message
    if check io.Copy(w, r) // return nil, error

}

func StartCopyFile() error {
  res := check CopyFile("1.txt", "2.txt")

  return nil
}

func main() {
  err := StartCopyFile()
  if err!= nil{
    fmt.printLn(err)
  }
}

Hello,

I've a simple idea, that's based loosely on how error handling works in the shell, just like the initial proposal was. In the shell errors are communicated by return values that are unequal to zero. The return value of the last command/call is stored in $? in the shell. Additionally to the variable name given by the user we could automatically store the error value of the latest call in a predefined variable and have it be checkable by a predefined syntax. I've chosen ? as syntax for referencing the latest error value, that has been returned from a function call in the current scope. I've chosen ! as a shorthand for if ? != nil {}. The choice for ? is influenced by the shell, but also because it appears to make sense. If an error occurs, you naturally are interested in what happened. This is raising a question. ? is the common sign for a raised question and therefore we use it to reference the latest error value that was generated in the same scope.
! is used as a shorthand for if ? != nil, because it signifies that attention has to be paid in case something has gone wrong. ! means: if something has gone wrong, do this. ? references the latest error value. As usual the value of ? equals nil if there was no error.

val, err := someFunc(param)
! { return &SpecialError("someFunc", param, ?) }

To make the syntax more appealing I'd allow for placing the ! line directly behind the call as well as omitting the braces.
With this proposal you could also handle errors without using a programmer defined identifier.

This would be allowed:

val, _ := someFunc(param)
! return &SpecialError("someFunc", param, ?)

This would be allowed

val, _ := someFunc(param) ! return &SpecialError("someFunc", param, ?)

Under this proposal you don't have to return from the function when an error occurs
and you can instead try to recover from the error.

val, _ := someFunc(param)
! {
val, _ := someFunc(paramAlternative)
  !{ return &SpecialError("someFunc alternative try failed too", paramAlternative, ?) }}

Under this proposal you could use ! in a for loop for multiple retries like this.

val, _ := someFunc(param)
for i :=0; ! && i <5; i++ {
  // Sleep or make a change or both
  val, _ := someFunc(param)
} ! { return &SpecialError("someFunc", param, ? }

I'm aware that ! is primarily used for negation of expressions, so the proposed syntax might cause confusion in the uninitiated. The idea is that ! by itself expands to ? != nil when it's used in an a conditional expression in a case like the upper example demonstrates, where it's not attached to any specific expression. The upper for line can't be compiled with the current go, because it doesn't make any sense without context. Under this proposal ! by itself is true, when an error has occurred in the most recent function call, that can return an error.

The return statement for returning the error is kept, because as others commented here it's desirable to see at a glance where your function returns. You can use this syntax in a scenario where an error doesn't require you leaving the function.

This proposal is simpler than some other proposals, since there's no effort to create a variant of try and catch block like syntax known from other languages. It keeps go's current philosophy of handling errors directly where they occur and makes it more succinct to do so.

@tobimensch please post new suggestions to a gist, and link that in the Go 2 Error Handling feedback wiki. Posts on this closed issue may be overlooked.

If you haven't seen it, you might want to read the Go 2 Error Handling Draft Design.

And you might be interested in Requirements to Consider for Go 2 Error Handling.

It may be a little too late to point out, but anything that feels like javascript magic bothers me. I'm talking about the || operator that should somehow magically work with an error intedface. I don't like it.

Was this page helpful?
0 / 5 - 0 ratings