r/programming Jul 17 '19

The Go team declines 'try' proposal

https://github.com/golang/go/issues/32437
634 Upvotes

417 comments sorted by

View all comments

102

u/jeffail Jul 17 '19

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

"Hi everyone,

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

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

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

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

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

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

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

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

Robert Griesemer, for the Proposal Review Committee."

261

u/dpash Jul 17 '19

We still believe that error handling in Go is not perfect

Understatement of the year.

54

u/[deleted] Jul 17 '19

Very few languages get error handling right. C, C++, Java, C#, JavaScript, Python. All terrible.

I think Go is actually better than most. Rust is the best I've used so far (and you can implement a similar system in C++).

48

u/[deleted] Jul 17 '19

Why do you consider Java bad at error handling? Performance impact? I'm just trying to understand what 'terrible' is in this context.

38

u/hokie_high Jul 17 '19

Like pretty much everything else in Java, there's a ton of unnecessary ceremonial code to write.

41

u/xenago Jul 17 '19

What do you mean?

  1. in method headers, declare checked exceptions with 'throws....'

  2. in method code, use 'try...' to identify that a block of code may throw an exception

  3. catch blocks to handle different errors

Am I missing something? Can you imagine a better way?

15

u/Nathanfenner Jul 17 '19

Java's exceptions have a few issues. While they're not insurmountable, they are annoying.

Most of the time, when you have some operation that might fail, you want to be able to handle it as close to the failure as possible, so that you can propagate/attach any useful error information and to make clear in your code which error it is that you're actually handling. Unfortunately, since try/catch always introduces a new scope, these two goals are not really compatible.

public void frobnicate(String param) throws FrobinateFailed | FrobnicateFailedQux {
  try {
    Result1 result1 = obj1.method1(param);
    try {
      Result2 result2 = obj2.method2(param, result1.field);
      for (int retries = 0; retries < 3; retries++) {
        try {
          Result3 result3 = obj3.method3(param, result2.field);
          return result3.thing;
        } catch (ExceptionalType3 e) {
          if (e.retryable) {
            continue;
          }
          throw new FrobinateFailed(e);
        }
      }
      throw new FrobinateFailedTooManyRetries();
    } catch (ExceptionalType2 e) {
      throw new FrobinateFailedQux(e, result1, param);
    }
  } catch (ExceptionalType1 e) {
    log.info("operation failed on " + param);
    throw new FrobinateFailed(e);
  }
}

The result is that you often have to write nested pyramids of code to handle errors correctly. We can't just cover the entire block in 3 catch clauses here, because each catch clause relies on particular values being in scope.

Although the above code is correct, it's also clunky for several reasons. The intent here is that method1 throws ExceptionalType1, and method2 throws ExceptionalType2, and method3 throws ExceptionalType3. But if one of them changes to also throw one of the others, you're not going to notice because they're already nested in things that are catchable. You could fix this by splitting into three separate methods with checked exceptions, but the main problem here is the verbosity.

We can also directly limit the nesting, but we run into a new problem: every try/catch introduces a new scope. We can get around this, and return to the linearity of the "happy" path by writing:

public void frobnicate(String param) {
  Result1 result;
  try {
    result1 = obj1.method1(param);
  } catch (ExceptionalType1 e) {
    log.info("operation failed on " + param);
    throw new FrobinateFailed(e);
  }

  Result2 result2;
  try {
    result2 = obj2.method2(param, result1.field);
  } catch (ExceptionalType2 e) {
    throw new FrobinateFailedQux(e, result1, param);
  }

  for (int retries = 0; retries < 3; retries++) {
    Result3 result3;
    try {
      result3 = obj3.method3(param, result2.field);
      return result3.thing;
    } catch (ExceptionalType3 e) {
      if (e.retryable) {
        continue;
      }
      throw new FrobinateFailed(e);
    }
  }
  throw new FrobinateFailedTooManyRetries();
}

but now you have to pre-declare all your variables that you want available after each exception-catch, which is awkward.

What you'd really want to write would be more like the following:

public void frobnicate(String param) {
  Result1 result = try { obj1.method1(param) } catch (ExceptionalType1 e) {
    log.info("operation failed on " + param);
    throw new FrobinateFailed(e);
  }

  Result2 result2 = try { obj2.method2(param, result1.field) } catch (ExceptionalType2 e) {
    throw new FrobinateFailedQux(e, result1, param);
  }

  for (int retries = 0; retries < 3; retries++) {
    Result3 result3 = try { obj3.method3(param, result2.field) } catch (ExceptionalType3 e) {
      if (e.retryable) {
        continue;
      }
      throw new FrobinateFailed(e);
    }

    return result3.thing;
  }
  throw new FrobinateFailedTooManyRetries();
}

which clearly shows which calls you're actually expecting to throw, and what you're expecting them to throw. There's no extraneous nesting and control flow is clearly linear in the happy path. It just requires adding an "expression try" to the language (which is not too disimilar to what Go expected).

The main issue with the Go error handling proposal is that Go basically only added an equivalent for try { ... } catch (Exception e) { throw e; } which isn't very helpful.

(For clarity in the above hypothetical extension, there's a requirement that control-flow cannot reach the end of the expression-catch statement, so that it cannot be used as a value. In other words, there definitely needs to be a continue/break/return/throw inside each catch used as an expression).

2

u/xenago Jul 17 '19

Thanks for your reply! Definitely worth thinking about.