If I encode my errors as values (usually with Either), I have to decide how to gracefully fall back if a failure occurs before I'm even allowed to use the successful result. Maybe I just hide the part of the view that needed that data. Maybe I show an appropriate error message. Maybe I email operations to let them know about the problem. Whatever I do, I have to actually think about it and not just assume that the error will be caught somewhere by someone. The result is usually a dramatically improved user experience when failures inevitably occur.
Exceptions tend to pass the buck so far down the line that there's no context to make an appropriate decision. Values tend to force a decision early, when you still have enough context. (Obviously both can be used against the grain, but the question is which pattern is easier.)
I beg to differ. It forces you think about whether but not why the function itself has failed. The "why" is embedded in the type of the error (or exception) itself, but Go does not force you to examine the type of the error; indeed, sheer muscle memory compels you most of the time to just write if err != nil again and again.
> I can decide how to gracefully fall back if a failure occurs before I'm even allowed to use the successful result.
You could wrap every line of Java code in a try/catch if you wanted to (it wouldn't be idiomatic, but it's definitely possible). You're just not forced to.
For what it's worth, you also shouldn't email your ops team directly from production code. It doesn't scale. You should log the error, and your monitoring stack should handle altering the relevant team (full disclosure: I work for such a monitoring stack company). It's very rare that you actually want to recover from errors, as that's typically a pattern that leads to silent failures and difficult to diagnose issues.
The lack of forcing to handle (checked) exceptions is exactly why I dislike Java's model. Until I've checked for an error state, I want to have an Either that may or may not have the data I'm looking for (and if it doesn't, has an explanation). In a truly exceptional situation I can crash and give a 500 error, but checked exceptions are by definition supposed to be recoverable, and in a production codebase I don't want to be able to lazily avoid recovering from them.
Java never lets you ignore an non-panicking exception. Unlike Go, where you CAN ignore the error value.
Besides that, Java forces you to explicitly state what type of exceptions each function can throw.
Java exceptions are stricter than Go, not vice versa.
Now, if we were talking about .Net or JS, or Python or mostly any other language with exception I'd get this criticism, but it is patently false when it comes to Java.
An attempt to read from a Reader, for example, can fail with a java.io.IOException. The javadoc for that lists 31 direct known subclasses, including CharConversionException and JMXServerErrorException; and there is always the possibility of a custom subclass from somewhere else in your application or a 3rd party library.
You can't do anything sensible with such a broad error (like decide if retrying might be sensible), so you end up either propagating it in your type signature, wrapping it in a RuntimeException, or ignoring it.
I agree that the Java standard library (especially the older parts of it) is quite bad. The Go standard library, whatever issues it has[1], is still pretty solid.
Unfortunately, the questionable quality of the Java standard library and the Java EE libraries back in the day, have led to some of the bad patterns we see nowadays in Java, that are not necessitated by the language, e.g. gross abuse of inheritance.
I still want to point out here that error values are not superior to exceptions, since:
1. Java shows exception handling can be forced to be explicit as well.
2. Handling Go errors is NOT forced. In fact, you can always ignore the functions' return value or assign the error part of it to a `_`. This is far less explicit than an empty catch block.
[1] https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-...
This is not true, and therefore your explanation is unsatisfying.
> You can write code as if every function call is successful
How does writing code as if every function call is successful (when you know that some functions will fail) not lead to some other bit of code further up the call stack having to make a decision about an exception that it doesn't have the context for? A bit of code which probably was written by a different developer who didn't anticipate what you were going to do?
There have been frameworks like the Common Lisp Condition System to let a caller dictate a recovery/retry policy, but they never caught on. In practice "do no harm, give up, and report the error" is what almost everyone wanted, and most languages support it without punitive effort.
Copy/pasting another person's answer:
You could wrap every line of Java code in a try/catch if you wanted to (it wouldn't be idiomatic, but it's definitely possible). You're just not forced to.
You can handle some of the exceptions in the same function, without going to the extreme of wrapping every line of code in a try/catch block.
Yes you ARE forced to. Unless the exception derives from RuntimeException.
In Go -- every error is obvious, and you get a sense of a cyclomatic complexity of any piece of code by just going through it quickly.
So, that's it for me. The complexity of the code is visible.
The patterns for error handling the golang have introduced are admittedly verbose, but they do lend a certain element of confidence that once the code is written, the errors should be handled. Of course a programmer can ignore the errors explicitly but doing so is different than forgetting to catch a thrown exception, because the programmer must go out of their way to write code ignoring the error. It feels like there's more agency around the decision.
Sure, you can write sloppy code in Java, but I am sure you can write sloppy code in Go too.
As you write code in a language with explicit errors, the language makes you acknowledge that the code you call can error out. This makes you stop and think what to do about the error. You can choose to ignore it, but that's a conscious decision that the language forces you to make.
With exceptions, there's no such feedback mechanism from the language/compiler. In order to write robust code you yourself must have the discipline to add exception handlers around the appropriate calls.
In short, defaults matter. It's simply easier to write correct, robust code when you don't have to go out of your way to do it.
See https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36...:
> It’s really hard to write good exception-based code since you have to check every single line of code (indeed, every sub-expression) and think about what exceptions it might raise and how your code will react to it.
... which leads us straight to favorite exceptions-caused bug: https://nedbatchelder.com//blog/202001/bug_915_solved.html:
> Inside tempfile.NamedTemporaryFile, the error handling misses the possibility that _TemporaryFileWrapper will fail.
You could ignore those, but similarly you could ignore err in Go as well.
"For the truly exceptional cases, the ones that represent either unrecoverable programming mistakes, like index out of bounds, or unrecoverable environmental problem, like running out of stack, we have panic.
For all of the remaining cases, any error conditions that you will encounter in a Go program, are by definition not exceptional — you expect them because regardless of returning a boolean, an error, or panicing, it is the result of a test in your code"
That's true in Java as well.
Java conditions you to view failure as an exception to the rule, and the happy path as the real code (which is the attitude you express in your first comment). Lots of us have since observed that this approach to failure leads to errors because programmers ignore failures, allowing them to be handled by a catch-all at the top of the stack, which typically just dumps a stacktrace and calls it a day.
The paradigm espoused by Go sees errors as just another program state, and one whose implications are just as important to consider as the desired behavior. This forces programmers to consider all the implications of a failure, rather than just adding another `throws` clause.
That's not typical Java code. You could do that in quick & dirty code, but I haven't seen such code in production code.
It sounds to me like you're an extremely lucky person. I've seen too much of that sort of thing in production code, including a catch-all for Throwable at the top that didn't even dump a stacktrace.
https://dave.cheney.net/2014/11/04/error-handling-vs-excepti...
https://dave.cheney.net/2015/01/26/errors-and-exceptions-red...
In other words, I also don't understand the hype around error values.
So, back to Python, what error value is suitable for replacing an exception? I mean, Python is flexible enough to do Go-style error values, but what does a good error value look like? Maybe I just haven't seen good error values?
If an error happens way down in the weeds and I initially return a very specific error value, do I just pass it up the stack forever? If so, how is that different than traditional exceptions? Do I replace the specific error value with more general error values as I move up the stack to higher-level code? If so, then I am throwing valuable information away and am likely to end up with something like "not found" as my error value at a the top -- not very useful.
I would probably also do some error logging, capturing the state of relevant local variables, especially if it is one of those cases where "see an error" is unexpected.
I suppose they're two sides of the same coin. Both can be done well or poorly.
I do resent the idea that error values are automatically better or easier to understand, because I'm dealing with some poorly done error values now and wishing for traditional exception handling.
Either is fine, I think. But, don't mix both in the same codebase. And with exceptions, I would really like Common Lisp-style restartable errors, to get the flexibility I feel I have with error values.
Why is that? I have never seen a cogent explanation for why this is the case.
I can tell you why exceptions (as implemented in Java) are cool: You can write code as if every function call is successful, as opposed to adding a line (or more) of error handling code after every function call, which makes it harder to follow the logic.