3 ms·
Exception based error handling is so bad and unsafe that adopting functional error handling with Either, Try etc as implemented by functional addon libraries fo
by bedobi 3y ago
Exception based error handling is so bad and unsafe that adopting functional error handling with Either, Try etc as implemented by functional addon libraries for many languages, while not yet common, in time it will become the new default even in OO languages. (just like it's been the default in functional languages for decades)
Functional error handling types are much simpler, safer and more powerful.
Simpler because they don't rely on dedicated syntax- they're just regular objects no different to any other object.
Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more. (no risk of ignoring errors and no risk of catching a higher level of error than desired, ubiquitous bugs in exception based error handling)
Powerful because they support map, flatmap, applicative etc, making it easy to eg chain multiple computations together in desired ways, which is unwieldy and bug prone when using exceptions.
> What is wrong about dedicated syntax
It adds complexity to the language! It could be that, when learning Java, Kotlin and any other language, we learn that methods return what they say they do... and that's that. No weird dedicated syntax and magic, special treatment for returning anything other than the happy path, and the HUGE complexity that comes with it, eg the dedicated syntax itself and how it behaves, differences between checked and unchecked exceptions, hierarchies of exceptions etc etc.
> Exceptions are easier
But that's the point, they're not.
Exceptions based error handling is unnecessary, hugely complex, doesn't compose at all, obfuscates or straight up hides what can go wrong with any given call, so leads to countless trivially preventable bugs... I could go on. And after decades of use, there's still no consensus about what exceptions should be or how they should be used. Exceptions are a failed experiment and I have no doubt that in ten years, Java, Kotlin and many other languages will acknowledge as much and move away from it the same way Joda Time outcompeted and replaced the horrible Java date and time library.
- jaen 3y agoOption and Result types, as implemented today in mainstream languages (ie. mostly anemically), are not the answer to exceptions being a mess. Exceptions have a lot of additional functionality in larger ecosystems such as: - Backtraces ie. showing the exact path of the error from its source to whereever it was handled, in a zero-cost way. This is by far the most important aspect of exceptions, as it enables automatically analysing and aggregating them in large systems, to eg. attribute blame from changes in error metrics to individual commits. - Nested exceptions ie. converting from one error system to another without losing information. Extensible with arbitrary metadata. - An open and extensible error type hierarchy. Again, necessary in large scale systems to differentiate between eg. the cause (caller fault, callee fault aka HTTP 400/500 divide), retryable or permanently fatal, loggable etc. exceptions while also maintaining API/ABI backward/forward compatibility. (for some of these, eg. Rust has crates for a Result-y equivalent, but a community consensus does not exist, yet...) General-purpose exceptions simply are complicated, and any system trying to "re-invent" them will eventually run into the same problems. Over-simplifying error handling just results in less maintainable, debuggable and reliable systems.
- bedobi 3y agoDon't agree with a single thing, especially not with the characterization that functional error handling is some kind of attempt at reinventing exceptions. But yeah, it's clear my and your camp will never agree lol. Fortunately for you, so far, your camp has mostly won, at least in the "object oriented" languages. But I think that's rapidly changing.
- jaen 3y agoI am not in any sort of "camp", in fact I prefer using a mostly functional style. The above comment was based on experience working in large (~100M LoC) code bases. As the comment clearly indicates, it is about anemic/"naive" functional error handling not being the counterpoint to general-purpose exceptions, not functional error handling vs. exceptions in general. I do mostly prefer error handling being explicitly marked at every call site (ie. the functional style), but note that this is not always meaningfully possible in very large systems (at least beyond the notion of "I do not know exactly what errors are possible here, just propagate whatever happens" which is equivalent to regular exception handling) And, as I already mentioned in the original, Rust does have functional solutions to some of these problems, and as other comments indicate, eg. Scala has them as well (probably even theoretically better since it can be a strict superset of the existing zero-cost exception model in the JVM).
- x-shadowban 3y agoThe backtrace argument is good, but I wonder how valuable traces would be in a world that never experienced reads-of-nothing (npe, reading from undefined, reading out of bounds array, etc). Presumably this would be because of 100% use of ADTs, or maybe some other mechanism; but, even Haskell throws exceptions out of `IO a` so such a world might never be realized.
- kagakuninja 3y agoThis isn't a binary choice. In Scala, you can use Throwable or Exception as your error type with Either: Either[Throwable, Option[Foobar]] The type Try[T] is essentially Either[Throwable, T] Either[Throwable, T], Try, as well as IO from Cats Effect give you the stack traces that you expect from conventional Java style, with the superior option of programming in the monadic / "railway" style. Try also interfaces nicely with Java libraries: val result: Try[Foobar] = Try(javaFunction).
- 1-more 3y ago> Exceptions are easier I'd add that they're easier because I don't know of any compiler that makes you handle every type of exception. I know a grip of compilers (and to be fair compilers with options and language extensions set) where you cannot not handle the "that didn't work, what now?" cases: the Left of the Either Left Right, Nothing of Maybe Value, Error of Result Error Value, None of Option etc etc etc. Languages that force you to handle everything that can happen when you do a thing are an accessibility tool for me and my fellow smooth brains. If I can't see it in my buffer I'm not going to remember to check it.
- seanmcdirmid 3y agoExceptions give you a nice stack trace, and you can configure your debugger to trap when the exceptional case occurs, rather than having to reverse engineer what happened when you see the functional error value. This can all be fixed, but right now for all implementations of function error handing via maybe and such) are very painful to debug.
- hot_gril 3y agoI'm happy with exceptions in high-level code, where like every line is possibly erroring out cause of a bad RPC, DB query, or even just bad integer math (div by 0 etc). If I'm writing a web backend or something, I don't want to manually handle every single error case. I just want to send back HTTP 4xx (or whatever equivalent in other protocols) when I can catch the problem and 5xx when I don't expect it. Probably most people in this situation agree. This is one of my beefs with Golang. The `err != nil` stuff gets exhausting when it's every other line. I get it if you're writing lower-level systems like you might in Rust or C, but Golang is often used for high-level stuff. Java, JS, etc did the right thing for their target use case. JS actually went more towards exceptions when they added async syntax, and if anything I think Golang is going to be the one to cave eventually (like they did with generics). And my day job used to use Erlang/OTP. It was a neat language with lots of great design decisions, but overall tedious for the type of feature work we were doing. It makes a lot more sense for things like ejabberd.
- bedobi 3y ago> I don't want to manually handle every single error case I get this all the time from people who are used to exceptions, and it's based on a lack of understanding. If you get an Either<Error, User> from the db, you don't need to handle the errors in every method call in the service layer. You can just call map and that function will be applied in the happy case, no need to handle the Error case. Then, at the resource layer, you can simply userService .getUser(userId) .fold({ error -> HttpResponse.400(error) }, { user -> HttpResponse.200(user) }) As for Go for HTTP REST APIs, agreed that it simply isn't a good tool for that. In fact, I don't think Go is a good tool for anything - for pretty much every use case, there are objectively superior languages, lol. But that's a whole nother can of worms.
- hot_gril 3y agoIn a language without exceptions, anywhere you call something that can return an error, you have to explicitly do something with it, even if you're just returning it early (which you usually are). That's what I meant by handling. Otherwise you're using exceptions, where early-returns are implied unless you catch. In your example, probably getUser is having to handle errors too, albeit with Erlang's rather slick syntax. I've been on both sides of this, preferring explicit handling when I was writing systems code and preferring exceptions when I was writing web backends, so I understand how to use each. Exceptions are popular in high-level languages because that's actually what people want in those use cases. I also dislike Golang for plenty of other reasons. "Master of none" language like you implied, and late to the party with nothing special to bring other than pointless syntax differences.