Hacker News new | ask | show | jobs
by lolinder 1864 days ago
I write Java, but I prefer errors as values exactly because you can't only consider the happy path. It really makes you think about the appropriate response to this specific failure. You can do that with exceptions, but in practice it's exactly like you say: all error handling is delegated to some generic catch-all, which in a web app usually just gives a generic 500 Internal Server Error.

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

3 comments

> It really makes you think about the appropriate response to this specific failure.

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.

Yeah, I'm not endorsing Go's approach to errors, just the idea of errors as values. I can't speak for Go, but other languages make it very obvious that to handle an error you have to inspect its type, and thereby get at the "why".

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.

You're differentiating between Java the language and Java the ecosystem. Java's tooling is so strong that you can use static analysis tooling to fail builds that throw checked exceptions, if you want. See e.g. https://rules.sonarsource.com/java/RSPEC-1162
That… sounds like exactly the opposite of what GP wants? If you can’t throw checked exceptions then you can only throw unchecked exceptions, whose catching and checking is even less enforced by the compiler.
GP is differentiating between library code and application code. You wouldn't turn on the static analysis to fail the build on checked exceptions in library code, since you want to force the application to keep track of known error modes in libraries and decide where it's appropriate to handle them. But you would turn them on in application code in order to prevent application code from using exceptions-as-control-flow, which is an anti-pattern and one of the reasons why people who value Go's error handling sometimes hold their opinions because of scarring they suffered from anti-patterns that were in Java codebases they worked with.
I'm not sure where all these comments which specifically mention Java are coming from. In Java you MUST catch or forward all exceptions besides those derived from RuntimeException, and you MUST specify them in the type signature. RuntimeExceptions are equivalent to go panics, so you don't have to catch 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.

I think Java in particular sticks in a lot of people's minds because, while the ideas are there, the execution is not - the standard library sets a poor example that is generally followed elsewhere.

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.

For what it's worth, I believe many I/O exception sub-classes came from before the time wrapping exception was established as a best practice in Java (as it has been in Go recently, with the introduction of fmt.Errorf("%w")).

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

> Exceptions tend to pass the buck so far down the line that there's no context to make an appropriate decision.

This is not true, and therefore your explanation is unsatisfying.

Can you please elaborate how the pattern that you describe doesn't pass the buck to a portion of code that has less context for the cause of the failure?

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

A library knows what happened, but not what to do about it. It doesn't know who the caller is. It could be an interactive user or a microservice RPC or a Spark job or a debugger.

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.

> Can you please elaborate how the pattern that you describe doesn't pass the buck to a portion of code that has less context for the cause of the failure?

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.

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

Yes you ARE forced to. Unless the exception derives from RuntimeException.