'If we didn’t have exceptions, we would have to write a lot of if-statements all over the place. [...] We can focus on the happy path.'
In my own experience in developing and supporting enterprise class software, focusing on the 'dark path' is much more important than the happy path. My mantra is 'first make it stable, then make it easy to change behavior while remaining stable, then finally add that functionality that the customer says is absolutely mandatory'. But this is very specific to BtoB with big players.
I think you may like Monads for exactly the reasons you described though, I wouldn't focus too much on this particular sentence.
Monads for error handling are very similar to exceptions - they let you write your code in terms of only the happy path, but letting each step fail the entire computation if something unintended happens (with a relevant error). They also have some advantages if you like to be explicit about the "dark path".
1) In traditional imperative programming you have to make a choice between errors as exceptions (implicit) and errors as return values (explicit) - monads let you do both at the same time in a sense --- whether you get implicit or explicit behaviour depends on how you "glue" computations together.
2) Monads are more explicit about when a dark path exists in the sense that they will have a different type signature and often different syntax (at least in haskell/c#/scala and other languages that support explicit monad-based notation)
I'm not a huge fan of Monads for error handling in languages like Javascript without explicit support for them though, mostly as they just don't read very well.
I’ve found exceptions fairly cumbersome to work with in ‘enterprise class’ software. Although I wonder if enterprise class software is cumbersome in and of itself, no matter how you write it.
Even in Ruby, handling the correct exception basically requires you to know that one is thrown, and then to know what kind of exception is thrown. So you sort of have to TDD your way through it. Or you just rescue every exception ever and hope for the best. You can still make it quite nice but I don’t think our modern brand of OOP has the intuitive approach here:
Haskell’s Either, Rust’s Result, Go’s facsimile of that with multiple return values, and Erlang’s approach to just letting things blow up are far easier for me to build a mental model around. There’s no guesswork: you don’t acknowledge the possibility of an error (let alone handle it), your build fails.
Even without them, you also have Maybe or Option to get rid of the accidental null values floating up the stack.
Indeed. For a while we focused on the happy path/MVP, and I was amazed how quickly edge-cases start piling up and sinking the ship with endless fixing/debugging/rework tasks. Now, we start with failure-cases first, engineer for resilience, and only then think about features. In consumer tech, failure rates of 2-5% often will not even trigger PagerDuty, but in enterprise that gets you fired real quick.
I would say this is why monads such as Either, and Optional, are so important. They require the programmer to deal with the dark path or at least make it explicit when there is a dark path. I first encountered these monads writing Scala years ago, and although it takes some getting used to, I always use them now in my Java day job to communicate the dark path.
Either fails at that, though. Yes, the programmer had to explicitly pull out one of the branches. Nothing guarantees the other branch was looked at, though. And with how it affects code, it is easy to read past where someone had a possible error and just ignored it.
Not saying using Either is pure win, but I think a lot of the benefits of the approach is that it's a lot more flexible for different actions you may want to take in response to errors. If you can reify the "throwing an exception" action as data (in this case, `Left(error)` then you can very easily for instance collect a bunch of these errors in a list or something, or perform other transformations. Or maybe you have a collection of data to perform work on, and only on the data that was associated with an error do you want to report, and the data that was not associated with an error, you want to process normally. It may be hard to write code that's flexible enough for all of these use-cases in a style that deals with errors by throwing exceptions up the call stack, but your mileage may vary.
Either and try-catch are basically the same thing, except try-catch is, as you say, baked into the language, while Either is just data, a value you can manipulate however you want. Usually they will have the same functionality, but in some cases Either gives a few distinct advantages (off the top of my head):
- You can work a list of Either values (Lefts and Rights). For example applying a readFile operation on a list of file paths can give you a list of Lefts (errors) or Rights, which you can handle later. I can't imagine doing that cleanly with try-catch (i.e. without building up some intermediate list).
- Can return whatever data you want in the error, rather than just a String message (or, I guess you can if you write some custom CheckedException, but Either saves you from having to do that every time)
Requiring functions to be "total" functions is another way to look at it. I like languages with pattern matching that will generate a compiler error if you don't handle all possible paths.
I guess you understood as much as I could envision it. Start your design phase by modeling all the forbidden state, errors, exceptions. Everything else is either 'good' or 'non critical'.
to mock TDD a bit: Paranoid Driven Development
But that's still a shower thought theory you know :)
It probably depends on the kind of software more than the industry, but in my experience, the unhappy path is usually 'bubble up until you get to some user input', with some careful use of destructors/dispose to undo some complex operations.
Exceptions cover this in the best way, since, unlike monads, they actually give you a pretty accurate idea of where the code was when the error happened,whule still playing very nicely with pure functions in the callstack ( which don't have to start adding error handling/logging logic simply because callbacks they use may fail).
In my own experience in developing and supporting enterprise class software, focusing on the 'dark path' is much more important than the happy path. My mantra is 'first make it stable, then make it easy to change behavior while remaining stable, then finally add that functionality that the customer says is absolutely mandatory'. But this is very specific to BtoB with big players.