Go Programming Experience

Intermediate

Other Languages Experience

Go, Python, JS, C#, Java

Related Idea

  • [ ] Has this idea, or one like it, been proposed before?
  • [X] Does this affect error handling?
  • [ ] Is this about generics?
  • [X] Is this change backward compatible? Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit

Has this idea, or one like it, been proposed before?

There have been some that are probably similar, but I don't believe there have been any quite like this

Does this affect error handling?

Yes. It is different than other error handling proposal because it doesn't really cut down a ton on the "make error handling take less lines of code", but I do think it improves error handling.

Is this about generics?

No

Proposal

There have been many language changes proposed to improve error handling. Many of these have had syntax changes to hopefully get rid of the so frequently used

if err != nil {
   return err
}

by adding some special syntax to quickly return. Such as err?. There have been a few common issues risen with these proposals:

1) Doesn't fit into the way Go looks/feels today 2) Adds a special keyword, and therefore isn't backwards compatible 3) The proposal only cuts down on lines of code and does nothing to increase the likelihood of properly handling errors. 4) A change of control flow, or at least on obfuscated control flow.

With this proposal, I aim to avoid all 4 of the above pitfalls. The proposal has three primary parts, all involving the enhancement of select statements. In my opinion, one of the issues with error handling in Go is that it is strictly a package level feature, not actually a language feature. This proposal would change that.

I propose the select statement be expanded to accept one parameter which, if provided, must be a nil or non-nil value of a type which implements the error interface. The select statement block is only entered if the error argument is non-nil. Its body syntax will be essentially identical to that of the switch statement, but behavior is a bit different for errors. Each case can be one of two format, except the default case which will have the same behavior and syntax as you would typically expect. The syntax for a case would either be case ErrIsValue: // case body or case varName as typedErr where as is a new keyword, but only recognized in this specific context, and therefore still backwards compatible. Each case is evaluated in the order it is provided. If the as keyword is absent, the behavior of matching the case is equivalent to checking whether errors.Is(err, ErrIsValue) == true. Similarly, if the as keyword is present, it is functionally equivalent to errors.As(err, &typedErr{})

Additionally, I propose a special "abbreviated" select syntax for errors that looks like this select err return "template string: %w", which behaves the same as select err { default: return fmt.Errorf("detailed message: %w", err) }.

Okay, that's enough talk with no examples! Here's what it looks like before and after the change:

Before Option 1

func handler(w http.ResponseWriter, r *http.Request) {
   if err := validateHeaders(r.Header); err != nil {
      if errors.Is(err, ErrUnprocessableEntity) {
         w.WriteHeader(http.StatusUnprocessableEntity)
         return
      }

     var badRequestErr InvalidFieldsErr
     if errors.As(err, &badRequestErr) {
        slog.Error("invalid headers", "fields", badRequestErr.Fields())
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    w.WriteHeader(http.StatusInternalServerError)
    return
  }

  // handle happy path
}

func validateHeaders(header http.Header) error {
   for key, values := range header {
      if err := validateHeader(key, values); err != nil {
         return fmt.Errorf("specific error message: %w", err)
      }
   }
}

Before Option 2

func handler(w http.ResponseWriter, r *http.Request) {
   if err := validateHeaders(r.Header); err != nil {
      var badRequestErr InvalidFieldsErr
      switch {
      case errors.Is(err, ErrUnprocessableEntity):
         w.WriteHeader(http.StatusUnprocessableEntity)
         return
      case errors.As(err, &badRequestErr):
         slog.Error("invalid headers", "fields", badRequestErr.Fields())
         w.WriteHeader(http.StatusBadRequest)
          return
      }

    w.WriteHeader(http.StatusInternalServerError)
    return
  }

  // handle happy path
}

func validateHeaders(header http.Header) error {
   for key, values := range header {
      if err := validateHeader(key, values); err != nil {
         return fmt.Errorf("specific error message: %w", err)
      }
   }
}

After

func handler(w http.ResponseWriter, r *http.Request) {
   err := validateHeaders(r.Header)
   select err {
   case ErrUnprocessableEntity:
      w.WriteHeader(http.StatusUnprocessableEntity)
      return
   case badRequestErr as InvalidFieldsErr:
      slog.Error("invalid headers", "fields", badRequestErr.Fields())
      w.WriteHeader(http.StatusBadRequest)
      return
   default:
      w.WriteHeader(http.StatusInternalServerError)
      return
   }

  // handle happy path
}

func validateHeaders(header http.Header) error {
   for key, values := range header {
      err := validateHeader(key, values)
      select err return "specific error message: %w"

     // Alternatively
    /// select err := validateHeader(key, values); err return "specific error message: %w"
   }
}

It can be argued that "Before Option 2" and "After" are not all that different. However, I think the code paths and cases are far easier in my proposal as each case is very simple and clear. Additionally, this proposal does more to make errors and error handling first class citizens in the Go language, rather than merely a package level feature with a built in interface. I believe cutting out the boiler plate will lead to people reaching more often to handle errors properly. Additionally, this proposal removes the need for if err != nil {} that bothers people so often, and provides a common syntax for handling errors in all cases.

Is this change backward compatible?

I believe it is!

Cost Description

Implementation is probably somewhat costly. Runtime cost should be no different than what we have today.

Performance Costs

Compile time cost should be negligible. Runtime cost should be no different than today.

Comment From: thediveo

In "Before Option 2" example, in the switch case clauses, I think the returns are missing?

Other than this very minor issue: this seems to rather tackle mainly ErrorAs? Comparing "Before Option 2" and "After" I don't see much difference, except for the var statement necessary due to ErrorAs?

Comment From: seankhliao

The select statement block is only entered if the error argument is non-nil.

I believe this would break a large amount of existing code for very little appreciable gain.

Comment From: chad-bekmezian-snap

In "Before Option 2" example, in the switch case clauses, I think the returns are missing?

Other than this very minor issue: this seems to rather tackle mainly ErrorAs? Comparing "Before Option 2" and "After" I don't see much difference, except for the var statement necessary due to ErrorAs?

Added the returns.

This change cuts down in verbosity in three key areas, making error handling easier to read and simpler to write.

1) ʼif err != nilʼ pretty much never needs to be written again, removing clutter. 2) The cognitive load to comprehend the functional equivalent of errors.Is and error.As becomes significantly less as the reader has less boiler plate. 3) Checking for specific errors becomes less verbose and repetitive, which I think will result in better error handling.

Comment From: chad-bekmezian-snap

The select statement block is only entered if the error argument is non-nil.

I believe this would break a large amount of existing code for very little appreciable gain.

@seankhliao please expound on how this would break existing code. I believe this is an entirely backwards compatible change.

Comment From: thediveo

The improvement looks small, especially when you deal with lots of "library" code that sits somewhere inbetween of the whole stack, where other proposals were trying to improve: the if err != nil return fmt.Errorf pattern. Given your own highly appreciated examples, this slightly reduces the ErrorAs boilerplate, as the explicit var declarations are becoming implicit. It is probably difficult to have statistical data here, so my own experience is just a single, subjective data point: I rarely have need for ErrorAs, and in those situations I world not benefit from this proposal. But as I stress, my very subjective experience.

Comment From: chad-bekmezian-snap

The improvement looks small, especially when you deal with lots of "library" code that sits somewhere inbetween of the whole stack, where other proposals were trying to improve: the if err != nil return fmt.Errorf pattern. Given your own highly appreciated examples, this slightly reduces the ErrorAs boilerplate, as the explicit var declarations are becoming implicit. It is probably difficult to have statistical data here, so my own experience is just a single, subjective data point: I rarely have need for ErrorAs, and in those situations I world not benefit from this proposal. But as I stress, my very subjective experience.

@thediveo

It makes error handling much cleaner imo. Also, I updated the examples to better demonstrate the fmt.Errorf pattern and my solution there.

Comment From: seankhliao

This would no longer work:

func main() {
    var err error
    switch err {
    default:
        fmt.Println("happy path")
    }
}

Comment From: chad-bekmezian-snap

This would no longer work:

go func main() { var err error switch err { default: fmt.Println("happy path") } }

@seankhliao that’s a switch statement. My proposal involves select statements.

Comment From: seankhliao

sekect is currently defined as:

A "select" statement chooses which of a set of possible send or receive operations will proceed. It looks similar to a "switch" statement but with the cases all referring to communication operations.

making it overlap with switch makes everything much more confusing.

Comment From: chad-bekmezian-snap

sekect is currently defined as:

A "select" statement chooses which of a set of possible send or receive operations will proceed. It looks similar to a "switch" statement but with the cases all referring to communication operations.

making it overlap with switch makes everything much more confusing.

It doesn’t overlap with a switch though. It has special behavior specific to errors only that you can’t get with a switch. I agree in some ways it may not be ideal, but it IS backwards compatible and once you’ve learned it I don’t think it is confusing at all.

Comment From: earthboundkid

A simpler proposal is to define ? as != nil and then use switch.

func handler(w http.ResponseWriter, r *http.Request) {
   err := validateHeaders(r.Header)
   var badRequestErr InvalidFieldsErr
   switch {
   case errors.Is(err, ErrUnprocessableEntity):
      w.WriteHeader(http.StatusUnprocessableEntity)
      return
   case errors.As(err, &badRequestErr):
      slog.Error("invalid headers", "fields", badRequestErr.Fields())
      w.WriteHeader(http.StatusBadRequest)
      return
   case err?:
      w.WriteHeader(http.StatusInternalServerError)
      return
   }

  // handle happy path
}

Comment From: chad-bekmezian-snap

A simpler proposal is to define ? as != nil and then use switch.

```go func handler(w http.ResponseWriter, r *http.Request) { err := validateHeaders(r.Header) var badRequestErr InvalidFieldsErr switch { case errors.Is(err, ErrUnprocessableEntity): w.WriteHeader(http.StatusUnprocessableEntity) return case errors.As(err, &badRequestErr): slog.Error("invalid headers", "fields", badRequestErr.Fields()) w.WriteHeader(http.StatusBadRequest) return case err?: w.WriteHeader(http.StatusInternalServerError) return }

// handle happy path } ```

That covers about a quarter of the feature set that my proposal does

Comment From: ianlancetaylor

Additionally, I propose a special "abbreviated" select syntax for errors that looks like this select err return "template string: %w", which behaves the same as select err { default: return fmt.Errorf("detailed message: %w", err) }.

This effectively makes the fmt package part of the language. Or, we have to carefully restrict the set of formats that can appear, which is confusing.

Comment From: chad-bekmezian-snap

Additionally, I propose a special "abbreviated" select syntax for errors that looks like this select err return "template string: %w", which behaves the same as select err { default: return fmt.Errorf("detailed message: %w", err) }.

This effectively makes the fmt package part of the language. Or, we have to carefully restrict the set of formats that can appear, which is confusing.

Yes. Agreed we should be cautious doing that. I also had the thought that we could add a new language feature available only within the context of an error select where, similar to the template packages, a dot represents the current “selected” error value

Comment From: thediveo

Additionally, I propose a special "abbreviated" select syntax for errors that looks like this select err return "template string: %w", which behaves the same as select err { default: return fmt.Errorf("detailed message: %w", err) }.

This effectively makes the fmt package part of the language. Or, we have to carefully restrict the set of formats that can appear, which is confusing.

Yes. Agreed we should be cautious doing that. I also had the thought that we could add a new language feature available only within the context of an error select where, similar to the template packages, a dot represents the current “selected” error value

A considerable part of my Errorf's add more context information than just wrapping an error, so this would end up in the rabbithole ianlancetaylor points out. Add to that the existing code doesn't seem to get boosted, as visible in your examples.

A generic error.As (ignore for the moment the name's taken) would be almost as good, if I'm not mistaken.

Comment From: chad-bekmezian-snap

Additionally, I propose a special "abbreviated" select syntax for errors that looks like this select err return "template string: %w", which behaves the same as select err { default: return fmt.Errorf("detailed message: %w", err) }.

This effectively makes the fmt package part of the language. Or, we have to carefully restrict the set of formats that can appear, which is confusing.

Yes. Agreed we should be cautious doing that. I also had the thought that we could add a new language feature available only within the context of an error select where, similar to the template packages, a dot represents the current “selected” error value

A considerable part of my Errorf's add more context information than just wrapping an error, so this would end up in the rabbithole ianlancetaylor points out. Add to that the existing code doesn't seem to get boosted, as visible in your examples.

Can you give me a concrete example of what additional context it is you're referring to?

Comment From: thediveo

A considerable part of my Errorf's add more context information than just wrapping an error, so this would end up in the rabbithole ianlancetaylor points out. Add to that the existing code doesn't seem to get boosted, as visible in your examples.

Can you give me a concrete example of what additional context it is you're referring to?

No idea what you mean by additional context, it's all in this issue.

Comment From: chad-bekmezian-snap

A considerable part of my Errorf's add more context information than just wrapping an error, so this would end up in the rabbithole ianlancetaylor points out. Add to that the existing code doesn't seem to get boosted, as visible in your examples.

Can you give me a concrete example of what additional context it is you're referring to?

No idea what you mean by additional context, it's all in this issue.

You said

A considerable part of my Errorf's add more context information than just wrapping an error

Are you saying you typically interpolate additional values? Or what do you mean by “add more context information”?

Comment From: thediveo

Interpolation.

Comment From: susugagalala

It is high time the Go team incorporates a "better" error/exception handling framework in Go. All the recent developer surveys atest to this dire need. I salute all the people who came up with proposals to this end, even if most if not all of them got rejected or even rebutted. How long more must Go programmers wait? Until the "IDEAL" error-handling framework comes up? My take is that rather than wait for the IDEAL, settle for one which is "not bad". It was the same with generics or parametric polymorphism. How long did it take for Ian, Griesmer, et. al. to finally come up an acceptable generics implementation in Go? Even Griesmer himself has said the current generics implementation does not entirely fit his ideal, but Go programmers everywhere have already embraced it! So, please! Don't wait for the "ideal" error handling framework for Go to appear. It won't!

Comment From: gophun

@susugagalala The Go team already tried it twice. The second proposal, which was an improvement on the first proposal based on feedback, was the most antagonized proposal in the history of Go proposals. You can find it by sorting the closed issues by most downvotes: #32437 By contrast, you can find the community's "counter proposal" (to "leave 'if err != nil' alone") by sorting the closed issues by most upvotes: #32825 So you can't blame the Go team for not wanting to repeat this experience. It's the Go community who really loves and defends the status quo.

Comment From: chad-bekmezian-snap

It is high time the Go team incorporates a "better" error/exception handling framework in Go. All the recent developer surveys atest to this dire need. I salute all the people who came up with proposals to this end, even if most if not all of them got rejected or even rebutted. How long more must Go programmers wait? Until the "IDEAL" error-handling framework comes up? My take is that rather than wait for the IDEAL, settle for one which is "not bad". It was the same with generics or parametric polymorphism. How long did it take for Ian, Griesmer, et. al. to finally come up an acceptable generics implementation in Go? Even Griesmer himself has said the current generics implementation does not entirely fit his ideal, but Go programmers everywhere have already embraced it! So, please! Don't wait for the "ideal" error handling framework for Go to appear. It won't!

Appreciate the passion. Can you share your thoughts on the proposal itself though?

Comment From: apparentlymart

This proposal is pretty interesting to me. I like that it focuses not on hiding the error handling but instead on establishing more explicit structure around it.

My main reservation was already stated: using the select keyword for this seems confusing, since this use of it is entirely unrelated to its existing meaning.

It also uses a different syntax for declaration and assignment than the current select does. For example, the badRequestErr as InvalidFieldsErr declares and assigns badRequestErr but does so without using the := operator, which is admittedly only a small inconsistency but an inconsistency nonetheless.

The latter is only a small concern. The first is more concerning to me, as this situation does feel closer to being a switch than it is to a select. I wonder if we could find a variation of it that does use the switch keyword, but does so in a way that's unambiguous with its two current uses.

Type switches already overload the switch keyword with a variation that has slightly different syntax, and so it would be consistent to overload it with one more meaning "error switch". I expect that an "error switch" would be syntactically similar to a type switch, since they are solving similar problems. I don't yet have a concrete idea for what to propose for that, though. (I will think about it some more.)

EDIT: I shared a switch-based variation of this proposal, which also made it non-error-specific, over in https://github.com/golang/go/issues/67372. I made it a separate proposal to avoid derailing this one, but since they both have similar motivations they also have a lot of overlap in what they are proposing.


Putting specific syntax concerns aside, I want to state some conclusions I made from the proposal to confirm whether I understood it correctly, and perhaps to prompt further discussion if I didn't.

In the case Identifier: situation, it seems that Identifier is just a normal value expression, expected to return a value of type error, which can then be passed as the second argument to errors.Is. This situation seems relatively straightforward, if so.

In the case a as B: situation, things seem a little more subtle. To say more about that, first here's my attempt at desugaring your "After" example, by which I mean that I think your example would be interpreted as equivalent to the following code:

    err := validateHeaders(r.Header)
    if err != nil {
        if errors.Is(err, ErrUnprocessableEntity) {
          // (this is the body of the first case, verbatim)
          w.WriteHeader(http.StatusUnprocessableEntity)
          return
        }
        {
            badRequestErrPtr := new(InvalidFieldsErr)
            var _ error = *badRequestErrPtr // (in other words: compile-time error if referent doesn't implement error)
            if errors.As(err, badRequestErrPtr) {
                badRequestErr := *badRequestErrPtr
                {
                    // (this is the body of the second case, verbatim)
                    slog.Error("invalid headers", "fields", badRequestErr.Fields())
                    w.WriteHeader(http.StatusBadRequest)
                    return
                }
            }
        }
        {
          // (this is the body of the default case, verbatim)
          w.WriteHeader(http.StatusInternalServerError)
          return
        }
    }

So: - InvalidFieldsErr must be something that would be valid to use as an argument to new, producing a pointer to that type. - InvalidFieldsErr must be a type that implements error. - badRequestErr must be an identifier that would be valid on the left side of a := *v operation, declaring and assigning a new variable.

Does that seem like a correct interpretation of the proposal?


Assuming I understood it correctly, I do like that it mainly just promotes existing error handling patterns to be a language feature that's harder to use incorrectly, rather than trying to hide the error handling altogether.

I expect it would encourage broader use of the errors.Is and errors.As functions (albeit by hiding that they are being called) and thus more use of machine-recognizable error types/values rather than just human-readable error strings. It does seem to mean either incorporating those parts of package errors into the language spec, or having the runtime include private functions equivalent to those which the compiler could generate calls to.

I feel broadly in favor of this, although I do think that overloading the meaning of select isn't ideal and hope we could find a variation that more closely resembles a type switch, using the switch keyword in a new third way.

Comment From: chad-bekmezian-snap

This proposal is pretty interesting to me. I like that it focuses not on hiding the error handling but instead on establishing more explicit structure around it.

My main reservation was already stated: using the select keyword for this seems confusing, since this use of it is entirely unrelated to its existing meaning.

It also uses a different syntax for declaration and assignment than the current select does. For example, the badRequestErr as InvalidFieldsErr declares and assigns badRequestErr but does so without using the := operator, which is admittedly only a small inconsistency but an inconsistency nonetheless.

The latter is only a small concern. The first is more concerning to me, as this situation does feel closer to being a switch than it is to a select. I wonder if we could find a variation of it that does use the switch keyword, but does so in a way that's unambiguous with its two current uses.

Type switches already overload the switch keyword with a variation that has slightly different syntax, and so it would be consistent to overload it with one more meaning "error switch". I expect that an "error switch" would be syntactically similar to a type switch, since they are solving similar problems. I don't yet have a concrete idea for what to propose for that, though. (I will think about it some more.)

EDIT: I shared a switch-based variation of this proposal, which also made it non-error-specific, over in #67372. I made it a separate proposal to avoid derailing this one, but since they both have similar motivations they also have a lot of overlap in what they are proposing.

Putting specific syntax concerns aside, I want to state some conclusions I made from the proposal to confirm whether I understood it correctly, and perhaps to prompt further discussion if I didn't.

In the case Identifier: situation, it seems that Identifier is just a normal value expression, expected to return a value of type error, which can then be passed as the second argument to errors.Is. This situation seems relatively straightforward, if so.

In the case a as B: situation, things seem a little more subtle. To say more about that, first here's my attempt at desugaring your "After" example, by which I mean that I think your example would be interpreted as equivalent to the following code:

go err := validateHeaders(r.Header) if err != nil { if errors.Is(err, ErrUnprocessableEntity) { // (this is the body of the first case, verbatim) w.WriteHeader(http.StatusUnprocessableEntity) return } { badRequestErrPtr := new(InvalidFieldsErr) var _ error = *badRequestErrPtr // (in other words: compile-time error if referent doesn't implement error) if errors.As(err, badRequestErrPtr) { badRequestErr := *badRequestErrPtr { // (this is the body of the second case, verbatim) slog.Error("invalid headers", "fields", badRequestErr.Fields()) w.WriteHeader(http.StatusBadRequest) return } } } { // (this is the body of the default case, verbatim) w.WriteHeader(http.StatusInternalServerError) return } }

So:

  • InvalidFieldsErr must be something that would be valid to use as an argument to new, producing a pointer to that type.
  • InvalidFieldsErr must be a type that implements error.
  • badRequestErr must be an identifier that would be valid on the left side of a := *v operation, declaring and assigning a new variable.

Does that seem like a correct interpretation of the proposal?

Assuming I understood it correctly, I do like that it mainly just promotes existing error handling patterns to be a language feature that's harder to use incorrectly, rather than trying to hide the error handling altogether.

I expect it would encourage broader use of the errors.Is and errors.As functions (albeit by hiding that they are being called) and thus more use of machine-recognizable error types/values rather than just human-readable error strings. It does seem to mean either incorporating those parts of package errors into the language spec, or having the runtime include private functions equivalent to those which the compiler could generate calls to.

I feel broadly in favor of this, although I do think that overloading the meaning of select isn't ideal and hope we could find a variation that more closely resembles a type switch, using the switch keyword in a new third way.

your desugared example is accurate, as are all of your observations

Comment From: ianlancetaylor

This introduces new syntax based on reuse of an existing keyword. It makes certain kinds of error handling cases slightly simpler. However, it doesn't address the biggest complaint that people have with error handling, which is the repetitive use of code like

    if err != nil {
        return err
    }

It's not all that common for people to check whether an error satisfies a list of conditions. When that is needed, we already have syntax for it. The syntax here is slightly shorter, but overall this doesn't seem to be a significant enough improvement to justify the language change.

Therefore, this is a likely decline. Leaving open for four weeks for final comments.

Comment From: chad-bekmezian-snap

This introduces new syntax based on reuse of an existing keyword. It makes certain kinds of error handling cases slightly simpler. However, it doesn't address the biggest complaint that people have with error handling, which is the repetitive use of code like

go if err != nil { return err }

It's not all that common for people to check whether an error satisfies a list of conditions. When that is needed, we already have syntax for it. The syntax here is slightly shorter, but overall this doesn't seem to be a significant enough improvement to justify the language change.

Therefore, this is a likely decline. Leaving open for four weeks for final comments.

Understood. It seems highly unlikely to me that we will ever get rid of explicit error checking for every error returned. To do so would almost inevitably lead to some kind of obscure syntax or new control flow— both of which have been reasons for many error handling proposals being declined. That being the case, it feels that the remaining options are those that leave error handling and explicit as is today, but provide marginal gains in simplicity or brevity. That is the goal of this proposal: provide a couple of small enhancements to error handling that hopefully, when added together, can alleviate distracting/noisy code from meaningful error handling

Comment From: findleyr

No change in consensus, so declined. — rfindley for the language proposal review group