5 ms·
Several comments here are along the lines of "I haven't done much monadic error handling but here's why it's bad." That's silly because the benefits of this ty
by dakom 7y ago
Several comments here are along the lines of "I haven't done much monadic error handling but here's why it's bad."
That's silly because the benefits of this type of programming can't be grokked until you use it on real projects.. it's honest-to-goodness not enough to look at small examples and judge whether it's worthwhile or not.
There's a lot of this realization going around actually - for example Rust's main raison d'être is basically: "look - we _can_ write safe c/c++, some of us can even nail it perfect every time in small examples, but in large apps it's inevitable that we'll all make mistakes"
Here's two issues with try/catch in real-world apps:
1. You aren't forced to write it. It's simply possible to forget that a function might fail (e.g. when that failure is defined as a side-effect). This is even worse when there's no type system forcing you to think about it. We all make this mistake!
2. You can't easily distinguish between expected errors and unexpected errors - and even where you can, dealing with that becomes a big mess.
Unexpected errors (like "out of memory", "computer is quitting", "electricity is gone") - _should_ crash and burn and stop the app (unless you explicitly have a way to recover from it - in which case it's actually an _expected_ error). Letting the app continue on where there's undefined behavior is dangerous.
Expected errors (like "user not found", "wrong password", perhaps even "network down") need to be dealt with. Every single time. Even in a function where it might not happen 99.999% of the time - that one rare time where it happens is especially hard to duplicate and fix.
That said, I agree with the comments that say it's a bit awkward in JS, and much less effective without a type system forcing the right constraints. I like using FP-TS[0] with typescript for extra help at "transpile" time, but Sanctuary[1] (based on FantasyLand[2]) with the runtime checking turned on is pretty incredible too.
With _some_ type system making sure that we're following the rules, monadic error handling is definitely more elegant and safer than try/catch.
[0] https://gcanti.github.io/fp-ts/modules/ https://gcanti.github.io/fp-ts/modules/
[1] https://sanctuary.js.org/ https://sanctuary.js.org/
[2] https://github.com/fantasyland/fantasy-land https://github.com/fantasyland/fantasy-land
- Klathmon 7y agoI pretty much agree with you. I hate error handling in JS. It's messy, it's really REALLY hard to get right, every single project has to layer on their own ways of handling it since the only thing the language gives us is the ability to `throw` and `catch` (even the "standard error" object doesn't have anything other than a place to put a string). It's a giant mess, and I'd love to find a way out, but I think it has to come from the language itself. Trying to shoehorn JS into a fully functional language leads to a lot of scaffolding and "custom" piping to get it to work, and in 100% of the situations I've seen it done it's been harder to maintain the scaffolding (and wrap 3rd party code to work in the way you need it to) than it ever would be to manage exceptions. I'll admit to be fairly ignorant of the different ways of handling exceptions in other languages. For the most part I've really only experienced try/catch, multiple-returns (golang style), and whatever bastardization JS is at now (try/catch + .then/.catch + custom error objects) in production codebases. But that solution needs to be part of the language not bolted on in userland. Mainly so that you don't need to write all this piping to get the rest of the ecosystem to behave in the way you want/need. FWIW Rust's Result type is beautiful and seems to solve a lot of issues (from the very small amount of experience I have with it), but trying to use that in JS is only going to lead to trouble without some help from the language itself.
- amalcon 7y ago> 1. You aren't forced to write it. It's simply possible to forget that a function might fail (e.g. when that failure is defined as a side-effect). When the compiler does require you to explicitly acknowledge exceptions (Java), people hate it.
- iainmerrick 7y agoYes, although I’ve never understood what the objection is. I’m sad that they removed checked exceptions from Kotlin (which is otherwise better than Java in almost every respect). When used sparingly, checked exceptions are great, specifically because the compiler forces you to catch and handle exceptions. Maybe if there were an easy to way to convert back and forth between checked exceptions and “Either” wrapper unions, that would fix the situations where checked exceptions are tedious or awkward to use. But I’ve honestly never felt that to be a problem, they’re fine.
- Retra 7y agoOne example: https://implementsblog.com/2015/04/07/better-checked-exception-handing-if-java-had-default-generics/ https://implementsblog.com/2015/04/07/better-checked-excepti...
- iainmerrick 7y agoYes, when you’re putting a class with exceptions inside a generic container that doesn’t allow for exceptions, it gets ugly. The solution in that blog post is clever, they should consider adding that to Java. Although, as the author touches on but doesn’t explicitly call out, this problem can be avoided in current Java simply by adding (generic) exceptions to your generic classes. Unfortunately that hasn’t been done for any of the standard library classes, old or new.
- chowells 7y agoPeople hate how Java did it. That's not the same as hating all possible expressions of the same idea. That's true of a lot of good ideas that were poorly executed in Java. Making it impossible to ignore potential errors is something I'm very happy to have as long as the tools for working with it are pleasant. Java's checked exceptions were decidedly unpleasant. Algebraic return types with combinators to work with them actually are pretty nice.
- jerf 7y ago"That's silly because the benefits of this type of programming can't be grokked until you use it on real projects.. it's honest-to-goodness not enough to look at small examples and judge whether it's worthwhile or not." But you can look at small examples and decide that the syntax noise isn't worth it. It's almost always a good idea to write "$LANGUAGE in $LANGUAGE" and not write "Fortran in $LANGUAGE" or "Haskell in $LANGUAGE". Importing these idioms into JS is pretty much the latter. You pay costs you don't pay in Haskell, and you don't get advantages that you can get in Haskell. It shouldn't be a surprise that the cost/benefit analysis for the constructs in JS can result in a very different one than the one for Haskell. You can't just substitute the analysis for Haskell and assume that it's all the same everywhere else. It's not "elegant". It's importing what is an elegant idiom in another language into a language where it's klunky, poorly-supported, and contrary to the grain of the language. It isn't elegant in JS just because it's elegant in some other language. It's exactly what it looks like: a klunky, choppy way to program in JS that requires the creation of a ton of functions that only a few outliers would normally think deserves its own function, making following this code require a lot of jumping around in the code base. And that's assuming it is what it says it is, which, as near as I can tell, it isn't. If I'm reading this right, it doesn't seem to properly short-circuit on errors properly, rather doing a rather odd thing where it tries to pass the value along down the line, which is at the very least going to have performance consequences and almost certainly major correctness consequences as this scales up. It's actually not even an implementation of "monad", but a sort of chopped up version to try to avoid creating even more functions, which is what is really necessary to correctly implement the "monad" interface. In Haskell do notation, every single "<-" is actually an entirely new function, nested inside the previous one, so: do thing <- whatever1 morething <- failableLogic arg thing things <- mapM stuff [thing:arg2] return head things If "failableLogic arg thing" fails, that function ends up returning, and the next two lines "don't execute", that is, the nested functions don't ever get invoked. The "return head things" is actually a function nested in a function nested in a function nested in a function. (I may be off by one.) I think the core reason there's a lot of blog posts about how to "implement monadic stuff" in all kinds of other languages and no code in the wild that actually uses any of it is the requirement for all the nested functions. (You usually can't "unnest" them either due to the fact you're almost always closing over values from the previous functions.) It is astonishingly inconvenient in languages that don't have the requisite features and support, to the point you can fairly reliably guess that if someone has an implementation that isn't staggeringly inconvenient to use, they've missed something fundamental. Even the languages that sorta kinda support this, don't, really. They encode specific bits of functionality vaguely inspired by monadic handling, but out of the Haskell family of languages I don't think I've yet seen a full, true implementation of "monad" in general that anyone uses. (When writing "$LANGUAGE in $LANGUAGE" isn't adequate for a task, it's almost always the right answer to go get a new $LANGUAGE.)