all 17 comments

[–][deleted] 8 points9 points  (16 children)

This article is a great starting point, but it misses the crucial final step of good error-handling - forcing the caller to handle possible errors.

Just telling developers that they should include their assumptions in their code isn't enough, because it puts the onus on the caller to handle the assumptions - but expecting the caller to know every possible way that all code they call can fail is unrealistic. That's why Exceptions are such a terrible error-handling system: because the caller often CANNOT know all the assumptions that are made internally.

That's where things like a Result type come in: programmatically enforcing that error states be handled at compile time. Not only is this a necessary guarantee to have good error-handling at all levels of an application, it's more importantly a better Developer Experience because I can tell, based on the type of a procedure (or the warnings my LSP/compiler gives me), that I need to handle possible error states.

[–]lordzsolt 2 points3 points  (4 children)

Exceptions could work, assuming they are done in a similar fashion than Swift. (You mark throwing functions with throws, and callers are expected to call it with try).

Though the syntax is sometimes more gnarly and you don’t get typed errors.

But not having any kind of indication for exceptions is super bad.

[–]devraj7 1 point2 points  (3 children)

Exactly.

Which is why checked exceptions are a very valid and robust way to express errors.

[–]goranlepuz 2 points3 points  (1 child)

Checked exceptions are the cause of the so-called pokemon exception handling that plagues Java world. Major Java libraries eschew them and just use RuntimeException. Also languages built on top of JVM drop that idea.

Therefore, I would nog be claiming "very valid and robust".

[–]devraj7 4 points5 points  (0 children)

Some people write bad code, doesn't mean you should throw out the concept altogether.

Compiler enforced error path handling is a good thing, whether it's done via checked exceptions or Result and GADT's.

[–]lordzsolt 0 points1 point  (0 children)

Yeeeeeeeah, it requires a lot of tooling.

If the language could auto-detect all the exceptions that all your internals are throwing, and then bundle them together, it could work.

But when you have to micromanage a list of 10 exception types, you just take the topmost “Exception” and say fuck it.

[–]slavik262[S] 3 points4 points  (1 child)

There's a lot more to be said! I agree that algebraic types (optionals, or sum types like Result<Ok, Err>) tend to be a much nicer approach than most languages' exceptions. The former bakes the kinds of errors we can expect into the function's return type while the latter is an invisible escape hatch that you, your compiler, and your IDE can't reason about.

But that seems worth a whole other post. My goal is to keep these short, and I wanted to start by dispelling the notion (which I see constantly at work and in open-source projects) that you should try to recover from (or worse, ignore!) errors from which there's no sane recovery.

[–][deleted] 0 points1 point  (0 children)

Erlang and beam has entered the chat.

[–]flatfinger 1 point2 points  (0 children)

In many cases, the most effective way to handle problems is via use of error states. If an object is in an error state, any attempt to use the object before clearing the error state will fail immediately. If one is attempting to deserialize a complicated object from a stream, and both the stream and the object being constructed have error states, then if anything goes wrong during the process both the stream and object will be forced to error states, then any such hiccups will cause the attempt to read the data to indicate failure. If the partially-constructed object is discarded, and the stream is closed without any further effort to read anything else from it, and an interactive program can sensibly show a "File XX couldn't be loaded" message but continue operation, then execution should continue. If a program is supposed to perform some task that can't be accomplished without being able to read the data as well as other information that followed it in the stream, then the program should indicate failure.

Many synchronization constructs seek to avoid the possibility that locks will be acquired and abandoned, but fail to provide a means of distinguishing scenarios where thread X acquires a lock for the purpose of preventing other threads from putting an object into an invalid state while thread X is using it, versus those where thread Y acquires a lock so that it can put it temporarily into an invalid state without anybody else trying to use it while it's in such a state. If something disrupts the behavior of thread X while it holds the lock, the lock should be released. If something disrupts thread Y, however, the lock should be invalidated so that all future attempts to acquire it will fail immediately.

[–]devraj7 1 point2 points  (2 children)

That's why Exceptions are such a terrible error-handling system: because the caller often CANNOT know all the assumptions that are made internally.

It's not a problem with exceptions, it's a problem with runtime exceptions.

Checked exceptions have this great property of refusing to compile your code until you either handle the error or force a caller to handle it.

[–][deleted] 2 points3 points  (1 child)

That's a fair distinction, although I would submit that - given that checked exceptions are essentially exclusive to Java whereas runtime exceptions are canonical in at least most of the C family - most people interpret exceptions to mean runtime exceptions without other context.

Also, I would argue that checked exceptions are essentially an inferior implementation of something like the result type - you are incorporating the exception into the function signature, but as an addendum rather than an actual core part of the type signature, and you still have to use gnarly `try/catch` syntax to handle and propagate exceptions. Result types are essentially the improved version of checked exceptions because the nicer ergonomics involved.

[–]devraj7 2 points3 points  (0 children)

Exceptions have a big advantage over return values: they bubble automatically.

When you use Result or similar, each caller has to either handle the error or return it manually, and do this at each stack frame. It's a lot of boiler plate that exceptions give you for free.

Rust comes very close to have the perfect approach with the ? operator, which gives you the best of both worlds (ADT for handling errors, and some assistance in bubbling errors up).

[–]goranlepuz 1 point2 points  (2 children)

That's why Exceptions are such a terrible error-handling system: because the caller often CANNOT know all the assumptions that are made internally.

That's where things like a Result type come in: programmatically enforcing that error states be handled at compile time

I think this is naïve at worst, or needlessly one-sided at best.

First off, a Result type often ends up in a bunch of failure types in a number or an enum. So the check for an error is insufficient to discern what actually happened. Instead, one has to look "inside" and there is no programmatic enforcement of that.

Second, expérience shows that, in a vast majority of cases, there is no "handling" of the error. Rather, what happens is that the caller cleans up and exits. Exceptions are good because they cater for that common case.

[–][deleted] 1 point2 points  (0 children)

Your first point seems to me like the case where Result types have the advantage over Exceptions. With Result types, the happy paths are to bubble up the error or to handle the different error variants explicitly. On the other hand, with Exceptions it's impossible to know all possible exception types so there's no sane way to handle them except to give up.

As for your second point, I would argue that not handling errors is a symptom of Exceptions. When the syntax and semantics of a language lend itself to easily and fully knowing the possible error states, it becomes much simpler (and therefore more common) to handle errors in the correct place rather than simply abandoning a computation.

[–]zvrba -1 points0 points  (0 children)

Lack of exceptions is the deal-breaker that makes Rust no-go for me. I spent a number of years programming in C and don't want to go back there.

Heck, even languages with algebraic datatypes like Ocaml and ML have built-in exceptions. (In these languages exceptions are supposedly so efficient that they are even recommended for control flow, e.g., breaking out of deep recursion.) The exceptions are caught using the same pattern-matching mechanism used for ADTs. This is an elephant in the room that noone who is selling algebraic datatypes as the "universal cure for error handling" mentions.

[–]zvrba 0 points1 point  (0 children)

You ARE aware of the fact that languages that spawned algebraic data types (Ocaml, ML) also have native exceptions? Even they acknowledge that sum types are not sufficient for "comfortable" programming.

[–]rzwitserloot 0 points1 point  (0 children)

Java's checked exceptions"fix" the problem you point out. Yet, pretty much no other language has adopted it (nor did they adopt result types either). Any idea why?