Hacker News new | ask | show | jobs
by yodsanklai 1749 days ago
> Programming with exceptions is difficult and inelegant. Learn how to handle errors better by representing them as values.

Funny how exception were invented because handling errors as values was considered to be tedious. And now, more and more languages are going backward.

4 comments

I think it's less strange than you think. In most languages that use errors as values, the tediousness is being directly attacked instead of trying to dodge around it. Haskell, in many of its uses, cleans up the tediousness so thoroughly that the code written using errors as values can be almost indistinguishable from code written using exceptions, and yet, nevertheless, the errors are values and no exception machinery is being deployed.

It has been a general trend in pragmatic programming languages in the past couple of decades. Another huge example, in my opinion, is in typing. Static typing in the 20th century was terrible. Tedious, broken, and missing a lot of its value. So a lot of languages were written that basically amount to a "screw that, we're not using types", and they became very successful. But in the 21st century, a lot of work has been done directly attacking the tediousness and problematic aspects of using static types, while also getting more value out of them with safer languages that more pervasively enforce them and make them more reliable, thus more useful, etc. So we're seeing a resurgance of the popularity of very statically-typed languages... but it's not "moving backwards" because it's not the same thing as it used to be.

Much like I don't expect dynamic languages to entirely go away, I wouldn't expect exceptions as we know them to go away either. But I expect "errors as values" to continue attracting more interest over time.

In fact, as test34's sibling post sort of observes, there's some synergy between these two trends here. Making strong typing easier has made it easier to have strongly-typed, rich values that can be used as error values and used in various powerful ways. Now that there are languages where it's much easier to declare and fully exploit new types than it used to be, it's much easier to just go ahead and create a new error type as needed for some bit of code without it having to be a big production.

> Haskell, in many of its uses, cleans up the tediousness so thoroughly that the code written using errors as values can be almost indistinguishable from code written using exceptions, and yet, nevertheless, the errors are values and no exception machinery is being deployed.

I don't have experience with Haskell, but I have mixed feelings about monadic error handling in Scala for precisely this reason. It goes to great lengths to recreate the programming ergonomics of exceptions, with exactly the same drawbacks. Monadic error handling, aka "railway-oriented programming,"[0] splits your logic into two tracks: a "good" track, where all your happy path logic lives, and a "bad" track, which is automatically propagated alongside your happy path logic. In my experience, it induces the same programmer mistakes as exceptions do: errors get accidentally swallowed (especially where effects are constructed and transformed,) different errors that require different handling are accidentally treated the same, and programmers fall into the habit of seeing the error track as an inferior, second-class branch compared to the happy path.

It confuses me when programmers (not talking about you, because I don't know how you write code, but people I've worked with personally) bash exceptions and then use monadic error handling to achieve exactly the same trade-offs.

This hasn't turned me off of monadic error handling, but it has made me think of it as FP's version of exceptions, rather than an upgrade. Personally, I think exceptions are a good enough trade-off in most cases, but when you need to be more careful, it is better practice to give all paths the same prominence in code. FP provides a better way to do this: pattern matching. More verbose, yes; harder to spot the happy path when reading code, yes; encourages more careful and thorough thinking about errors, for me absolutely yes. YMMV.

[0] https://fsharpforfunandprofit.com/rop/

> This hasn't turned me off of monadic error handling, but it has made me think of it as FP's version of exceptions, rather than an upgrade.

This reminds me a lot of Java's checked exceptions just with different window dressing. You move the failure mode type information from the exceptions list ("throws" clause) into the return type.

Typed error return values is definitely an improvement in ergonomics over C-style error code returns, and pattern matching is definitely a big improvement on ergonomics too, but I think error handling has a problem of fundamentally irreducible complexity. For example, if you make network calls, you have to be prepared for network calls to fail, and you have to design your system to recover from it somehow, whether that happens in a try/catch or a match on Either[Throwable, Result].

I think trying to reduce the complexity of error handling is the original sin. Thinking of error handling as a separate case requiring separate mechanisms is the original sin. The structure of your code should not reflect any difference between "error" and "success" cases. They are equal, and neither should be subordinated to the other.
I've started writing a blog post that covers this topic, and once I started thinking clearly about it, I've found errors to be a really hard problem.

To even get out of static vs. dynamic typing, I've expressed the problem as "Given this piece of code, how can I know what types of errors will come out of it?", where I'm using a human, loosey-goosey sense of the word "type" here, rather than necessary a strict type. (If your language wants to answer that in terms of strict types, great, but I'm trying to answer it very generally across programming languages.) It turns out that from what I can see, the underlying problem is that "errors don't compose"; given a function f that returns errors X, Y, and Z and accepts another function f2 to call that may return other errors, it is really difficult to characterize f(f2) in practice. For a concrete, static f2 we can mostly at least imagine taking the union of the two (although even that can be an oversimplification; what if f calls f2 in such a way that one of the types of errors that f2 can produce is guaranteed not to happen, e.g., what if f2 could throw a null pointer exception but it can be easily statically proved that f never passes it one?), but once you let enough polymorphism into the mix to let f2 be an arbitrary closure of some type, all bets are off in terms of what f(f2) can produce in most languages.

This hurts statically-typed languages in that they can't create very strong types for these sorts of situations, but more generally, translating the theory up to practical experience regardless of the language being used, A: it's hard to program in an environment where you have enough polymorphism of some sort (OO, accepting closures, whatever) to have errors mix like this in your code and know what sort of errors may occur where and B: that's nearly everything because programming in an environment that lacks that polymorphism is not something we generally do voluntarily. (Embedded code not allowed to even allocate on the stack is this static, but we can't build everything that way.)

Amusingly, I think what saves us in the end is that to a first approximation, there is no such thing as error handling. All there is is logging something for a human and giving up. Obviously, to a second approximation there is a such thing as error handling. I've got plenty of it. But honestly, it's pretty rare by percentage. Most errors result in a log message and some level of failed task. Fortunately, with some work, we can usually get our systems fed enough good data that we can do things without errors, such that the ones that do occur end up almost always being essentially correctly handled by screaming and dying. If programming actually required us to handle errors, like, in some intelligent manner all the time, we'd have a lot fewer programs in the world!

Well, on the other hand there's difference between handling errors with values like:

-1, 0, 1 and other obscure things

and using proper types like

Result<T>

Yes, errors-as-values only works well with a type system which supports discriminated unions. And programming languages with such support have only recently become popular.
OCaml is a somewhat old language with algebraic types (so including discriminated unions) yet exceptions were introduced precisely to avoid dealing with propagating errors as a values. I agree with your point, but would argue that even with sum types, propagating errors is tedious and there's a case for using exception.

I wonder if another reason why errors as value are making a come back is because of the asynchronous programming style which is becoming quite pervasive, and doesn't play well with exceptions.

More idiomatic error handling has appeared too. It's become a lot easier to bubble up errors with messages (e.g.: golang) without handling every single case.