Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Really nice article that covers quite well the complexities involved in dealing with errors. "They're just another kind of value" and "Just crash and restart" are both wrong, error handling is much more complex than that.

Different kinds of errors need to be handled in different ways in various contexts as the article points out.

I won't comment on the error model proposed in the article as I find that without actually trying out stuff like this it's very hard to get a "feel" for it, but at first glance it looks a bit over-engineered.

I think the gist of what makes a good error handling system is as follows:

Certain errors are contract violations/bugs that should not occur in normal program operation, and should not "taint" the code with explicit error handling. The language should provide a mechanism that allows to abort the current "task" when such an error occurs and trigger some error handler. Exceptions are good for this, but it falls on the programmer to define a "task boundary" appropriately. Defensive try/catch all over the place is not that, and is usually done because...

Certain errors are part of normal program operation and the programmer should be reminded to deal with them. Exceptions are terrible for this but Result<T,E> types are great. Having to use Result<T,E> in situations where the error is actually not normal part of program operation (e.g., you're accessing a key on a map that can only be missing due to a bug) is annoying but is easily solved by providing two separate APIs (like Python's __getitem__() and get() methods on dict).

Often times an expected error has no good way to be dealt with at the point it occurs, so there should be an easy way to just abort the current "task" when that happens. This is your ".expect()" or just a giant pile of ? propagation in Rust but Rust panics don't carry enough information by default like you can with an Exception type. Note that is different from the two API version above. You use the "panicking on error" API only in cases where the error is not expected, whereas you use some .expect() equivalent as a way to abort the current task for an expected error.

Sometimes you also want to collect multiple errors and abort only after every error is collected, like in a typechecker. This is often done with "NullObject" patterns or "ZII" (most of the time handled very poorly) but there are ways to do it right where the error case "poisons" the data up to the task boundary but cannot propagate further than that (turning into a Result<T,E> equivalent at that point).

The most important part is understanding task boundaries. I find most programs (including my own) do a very poor job of this.

The information contained in the error (sometimes called the context) is very tricky to deal with as what constitutes relevant information for a library or for an app using that library can be very different and there's a performance cost involved with its collection.



Well, arguably Java had the right idea with their checked/unchecked distinction.

I believe effect types (that are the true generalization of checked exceptions) are a pretty good solution to error handling, though as you also write, certain functions where an "error" case is expected should just use ADTs or so (e.g. parsing a string to an int)


While people love to put this on Java, the idea predates the language by at least two decades, having appeared at least in CLU, Modula-3 and C++, before Java came to be.


Effect types have many good properties, the most important of which being that they provide clear semantics for the composition of multiple different kinds of effects (like exceptions and async).

But I disagree that they are a good solution to "error handling", because a single solution cannot cover all situations. For example, should the indexing function on arrays have an error effect? It'll taint every function that indexes an array even if the function cannot possibly go out of bounds. Same thing with situations like integer overflow, should every function that does integer arithmetic have an error effect?

Unchecked exceptions (or "panics" as the new languages like to call them, rose by any other name and all that) are clearly the superior solution for situations like that, what I like to call "contract violations".

The Elm language does not have effect types, it just uses Result types, and is vehemently against exceptions. The outcome of that is that division by 0 returns 0 because tainting every function that divides two integers with a Result type would be bonkers. Personally I find turning an erroneous situation into a "safe default" that isn't really safe to be a very silly thing to do, specially for a language that proudly claims to have no runtime errors (just logic errors instead I guess?)

At the other end you have Python iterators throwing an exception when they complete, which is one of the weirdest design decisions I've ever seen. How is an iterator reaching the end of the thing it is iterating an exceptional situation?

This last one really boggles my mind.


Agreed on the crucial aspect of task boundaries. Errors should be dealt with as close to the edge of the task as possible, and tasks should be cleanly separable from one another. Think of an app as a small driving program that uses many libraries written by others. Although it takes much more effort, I believe it is an overall better design for clarity, error handling, flexibility, and whatnot.


> "They're just another kind of value" and "Just crash and restart" are both wrong, error handling is much more complex than that.

Regarding the first slogan: it is correct that they are just another kind of value. But they look more complex. The reason isn’t the error values as such. The reason is that they always (except for `None | Error`) appear together with a rich normal-value. In turn they end up looking more complex, simply by context-association.

And treating errors as “just another kind of value” necessitates general value-manipulation facilities. Because we expect to manipulate normal values in whatever ways we want. But for errors we tend to get stumped once we want to do something else than whatever the default is, which might be (using your example) to accumulate errors instead of bailing out after finding just one.

But we tend to get stuck trying to simplify errors and their processing too much. When really we should move towards generality; the “just” in “just another kind of value” should mean that we can use a lot of whatever we have (not invent new things). “Just” shouldn’t hint at “and so it’s easy/simple”.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: