Thought Firefox was broken when I looked at the code samples, but then I tried it again in Chrome and thought my graphic card was going crazy. Turns out the author actually chose a font that looks like it's been printed on paper with poor ink, for the code examples. Would have been better for readability to leave the code examples to some more normal looking font, or at least just set `font-family: monospace` and let the OS decide.
The code samples look okay for me (did the author tweak the stylesheets?), but the text uses an 'engraved' look using CSS's text-shadow (like iOS Notes but a bit worse because of the serifs) which makes it a bit hard to read.
This is where browsers' reader modes really help. Is Chrome the only one now that doesn't natively support reader mode? Reader mode in Firefox made this much easier to read.
The arguments here against try/catch are superficial and in no way compelling enough for me to adopt this pattern for error handling. Seems to be a solution looking for a problem... stop trying to be so clever - focus on writing clean, readable, simple code.
Agreed. I never understood the love for Either-based error handling in Haskell and other languages. It's such a bare bones system. If you add all the features that people want from an error handling system, like stack traces; and remove the misfeatures that nobody wants, like Either<IOError,Either<MathError,Result>> being a pointlessly different type from Either<MathError,Either<IOError,Result>>; you end up with (surprise!) try-catch.
I'm not familiar with Haskell, but Rust's `Result` type is the same idea, and I wouldn't want to trade it for try-catch. It supports backtraces[0], has a simple way to turn errors into a "catch-all" type[1], but it is significantly less complex since it reuses the basic return value mechanism a function has.
If you want to exit the program because the error can't be handled (similar to an unhandled exception), rust has a different concept for it, panics, and any Result can be turned into a panic via `unwrap()`.
IMO both are important: panics represent unrecoverable errors, while Results can be acted upon (for instance, if read() returns Err(EINTR), you'll want to retry the read)
[0]: Granted, you need to write some boilerplate to get the backtrace on error creation.
[1]: You can cast all errors to `Box<dyn Error>`, a heap-allocated dynamically dispatched Error type.
> If you add all the features that people want from an error handling system, like stack traces; and remove the misfeatures that nobody wants, like Either<IOError,Either<MathError,Result>> being a pointlessly different type from Either<MathError,Either<IOError,Result>>; you end up with (surprise!) try-catch.
What does a function that doesn't error look like under try-catch? How do you tell it apart from a function that can error.
It's the same argument as null/Optional. Yes, Optional behaves the same as a nullable value; the point is that languages with Optional allow you to have values that aren't nullable.
Edit: Parent was edited, previously suggested "Java-style exception specifications". As far as I know every language with "throws clause"s has the kind of problems I describe below though.
Three major problems: they interact inconsistently with generics, you can no longer take a function's result and store it as a value, and you still can't tell whether a function might throw by looking at the point where it's called.
A good version of exception specifications would be lightweight syntax sugar over either (in the same way that a good version of async/await is a lightweight syntax sugar over futures/promises). But I'm not aware of any language implementing that; I guess once you have working Either there's not enough value in providing an alternative syntax for it.
Actually I thought a bit and realized that I don't want to defend checked exceptions. Let me amend the previous answer to "I prefer such functions to be interchangeable".
If those functions are interchangeable then you potentially have to deal with exception control flow on every line, or even within a single line. It makes it very hard to reason about all possible paths through a given function and do things like safe resource management (e.g. you always have to handle the possibility of further exceptions in your exception-handling code path).
For system failures it's ok because the only handling you're going to do is retry at high level (and so even languages with an Either-oriented style tend to have some form of "panics" or exceptions for that kind of failure). But it can be a real problem when that also happens for "normal failure" paths like bad user input where you want to actually have business logic that handles and perhaps recovers from the failure.
why is either wrapped in eithers in your example? That feels like wrong code to me. Wouldn't it be Either<(MathError|IOError), Result>, and you pattern match the error or result?
The types are definitely isomorphic, but the difference between them is not "pointless". You could have Either<IOError, Either<IOError, Result>> and want to preserve the information about which step led to an IOError outcome.
That information shouldn't be in the type, because it makes functions pointlessly non-interchangeable. With try-catch, a superset of that information is preserved in the stack trace, and functions stay interchangeable.
I consider the error types to be a useful information that should absolutely be part of the method type.
If a method can fail on IO that's different than if it cannot fail. And the surrounding code has to account for that.
So, you might think that the methods are interchangeable if the error type is not present in the signature but this is actually wrong. Different failures have to be handled differently.
(I assume that you are /not/ talking about valid error cases in the problem domain that also might have to be handled)
> stop trying to be so clever - focus on writing clean, readable, simple code.
That's exactly what using either instead of try/catch does! try/catch are magic language keywords that create invisible, surprising control flow. Either is plain old code written in the normal language, with functions that follow the normal rules.
Fully agree. IMO this is the same argument as "the for loop is a magic keyword" which just isn't true.
Try/catch makes it _incredibly_ clear what you're trying to do. Kids who graduate college and get their first real job understand what a try/catch does. A random contributor to an OSS project knows what try/catch does.
Meanwhile, I'd be willing to bet that not even 10% of programmers could actually explain what a monad is.
I'm sure there is a place for this elegant error handling, but in most codebases it seems like a pretty big complexification for not all that much benefit. Sure, the code might even be "more correct" (whatever that means), but if Samantha the intern can't pick it up rather quickly, it probably isn't all that well suited for mainstream usage.
> IMO this is the same argument as "the for loop is a magic keyword" which just isn't true.
The for loop is indeed a magic keyword, though it's less surprising/magic than try/catch; most of what for does could be done by a plain old function.
> Meanwhile, I'd be willing to bet that not even 10% of programmers could actually explain what a monad is.
Don't try to generalize prematurely. Considering Either on its own, it's simpler than try/catch and can replace their use cases. If you'd started with Either, try/catch would seem like the overcomplicated solution in search of a problem that it is.
Haskell doesn't have special support for Either. It's a plain old data structure with a few type classes. You can write Either in any language that supports passing functions as parameter values.
It's taking the logical disjunction operator, `||` in many languages, and sticking it in a data structure. You can now pass it around like a normal JS value and combine it with other such values.
The benefit of this over exceptions is that for unexceptional situations you don't end up throwing away the context of your computation if something takes the "bad path." Instead of jumping to the exception handler and losing all of your data your program can handle the situation at the site of the error where it has the most context to solve the issue.
TFA wasn't advocating abandoning try/catch -- it was suggesting that for non-exceptional cases it will make your code cleaner.
This argument doesn't hold with me, because while try-catch is very clear in what it does, the exception-/throwing/ mechanism is not. Try-catch is only useful insofar as you know exactly what you're trying to catch. Anything else is basically undefined behaviour; a jump straight to your catch block from anywhere in the application, as far as your intuition is concerned, a far cry from the controlled nature of loops.
It depends on how it is used. I’ve definitely seen it used like a goto mechanism (on the JVM, i don’t write much JS), e.g. throw x in y to skip these business logic steps in z, and that makes my head hurt.
Personally I just try not to throw exceptions when compensation/recovery is expected. If something should be handled by the caller put it in the return type, if it’s really fatal give up and log a stack trace.
> All language keywords are “magic” to some degree.
Agreed. Every keyword adds complexity to the language; the fewer your language needs, the better. If your language has first-class functions and polymorphism (and any serious language does these days), there's no need for special-case control flow keywords; better to have a design like e.g. Smalltalk, where if/while/... are just normal functions.
> The control flow that try/catch creates has never been surprising or invisible in my experience.
One of the biggest production bugs I saw happened because of removing an unused variable (the function looked correct, but actually the unused variable right at the top of the function was throwing an exception; the fallback code path was correct, but the function itself was implemented wrong). It's the same problem as https://glyph.twistedmatrix.com/2014/02/unyielding.html - you can't tell which function calls might throw by looking at them, and most of them don't, but some of them do. And on top of that you have the goto-like "action at a distance": starting from a given catch there's no way to find the corresponding throw (or vice versa). You can't even tell when a catch is completely unused.
Easily refuted observing that you could strip languages of many of their keywords (for example replacing for and while with if and goto), and you'd end up with less readable code. Keywords are often added to make languages simpler, at least because they declare the intention of the developer.
Otherwise, Brainfuck would be the least complex language to write code in.
You're conflating the complexity of the language itself with the complexity of code written in that language. Brainfuck is a famously simple language; brainfuck codebases are quite complex. Sometimes building complex functionality into a language might be worthwhile, if it allows you to simplify code written in that language. But since a developer working in the language has to be able to understand both the language and the codebase they're working on, you need to be careful about that tradeoff, only adding to the language those features that are general and powerful enough to simplify a lot of codebases. Otherwise you blow the whole complexity budget on language functionality, leaving the developer with no spare mental capacity to understand the specific codebase - even if their codebase makes no or minimal use of some of those complex language features.
Better, where possible, to implement general-purpose reusable functionality in libraries written in ordinary code in the language. That's a win-win approach: reusable library code can simplify codebases written in the language, but since it's plain old code that follows the normal rules of the language, a library doesn't add language complexity that the developer is forced to keep in mind.
Fundamentally, the key to making codebases in your language understandable is to be compositional: make sure that the developer can easily understand the combination of a and b if they understand a and b separately. Library functions do that, because you can understand how code that calls a library function behaves without needing to understand what the library function does. But language features like throw/catch don't do that: you need to understand what throw does if you are to understand code that calls a function that throws, even if the code you're trying to use doesn't throw itself.
Sounds like you’d enjoy an R5RS scheme. Minimal syntax goes a long way. Unused variables throwing exceptions definitely seems like an over accumulation of technical debt.
> try/catch are magic language keywords that create invisible, surprising control flow.
I'm surprised people believe this. If you added multiple returns to every function and manually propagated every error you'd get exactly the same thing as try/catch. It's existence comes from not wanting to do all this pointless boilerplate and instead having the compiler do it for you.
I don't use this pattern to deal with the problems in my day to day JavaScript code in case other people confuse. I just think it's unfair to simply believe this is complex. It's simpler on mental. I'm not encouraging people to use this style, it's just some random defense.
This pattern is not more complex than try/catch. It's just JavaScript lack constructs to make the writing much less verbose. (we could abuse async/await but it's tricky).
Try/catch is an extra language construct which is confusing - it has dynamic semantics just like JavaScript's weird 'this' behavior. And it's leaky just like null - when you forgot to handle something, the whole thread explodes. Besides, it's not an expression, which means it's harder to refactor or return with function, passing as an argument, etc. On the contrary, Either would be just fine everywhere, just typical functions and values.
Another use case is when you want to collect the errors along the road, instead of failing fast behavior, in this case, Either would be easier than try/catch.
It may not be complex, but it's certainly obscure. Any JavaScript developer could understand the try/catch example but many would struggle with the Either example.
Saying something is superficial and too clever is not a counter argument, but rather a reaction, I think, to it being radically different from what you're used to. I'm not saying it's necessarily better, but maybe approach things with an open mind.
I'm sorry but this just seems so hard to read and I feel like it would be even harder to maintain.
I'm currently working in a codebase that pulled a ton of this functional style into javascript and it's overwhelmingly complicated and difficult to follow even for me who has a bit of "functional" programming experience (mostly ocaml).
Maybe I'm just missing something or maybe i'm just dense, but I haven't ever personally seen a situation where the complexity of setting up and using this functional style in javascript has ever been more readable or maintainable than the equivalent "traditional" imperative programming. It's one thing if the language or ecosystem kind of enforces this and gives you a standard set of tools to work from or helps with this style via language constructs, but javascript is not that language.
For the exact example in the article, both the try/catch version and the "nullcheck" version seem much simpler to understand (even if they are admittedly a bit "ugly" looking). And while I get that the author is using a simplistic example to help teach, it really doesn't seem like a more complex example would change things.
Readable functional programming imo really hinges on some essential basic language primitives. Those are sum types, pattern matching, and restricting mutability.
Forcing sum types onto a language that does not support them is bound to end in awkwardness and reduced readability without providing the real benefits of increased
correctness.
This is a great example. std::option from C++ is another one.
Yeah it's really hard to introduce, let alone completely stick with, FP in a non-FP language. I started to introduce some into our Ruby/Rails project, but halfway through, the code was more procedural than functional (which admittedly is more concise and cogent than OO); it's really hard to remember to treat everything as immutable all the time.
When combined with flow or typescript, Javascript absolutely supports sum types. Also, it's trivial to encode the Option and Either monad using Objects to get the same functionality.
Fully agree. I'm not a JS expert by any means, but I dabble here and there, mainly for work.
If someone did this at work, I'd wonder why they are trying to add so much complexity to something that is rather simple and already well understood by most.
It would be hard to convince me to sign off on a code review where this was added tbh. I'd love to hear how/why I'm wrong here though.
Maybe there are better use cases for it? But in my case, the major thing I care about is that code is simple and readable.
Samesies. Worked with a bunch of folks who liked bringing functional patterns into JS when that started to get trendy, and I got it, but I never got it. Like, why. Never felt like it was making my life easier.
When I've felt the best writing JS was back when it was a much worse language (which is saying something) and I wrote it about as close to C as I could manage. But then I've always thought the prototypal object model is a giant footgun with little legitimate purpose and any "clever" use of it's simply a bad idea, so maybe I'm just inflexible. I also think modern classes are entirely fine, even good, for Reactjs, and adding more patterns just to be more "functional" (but not really—look under the hood) is a very bad idea, but I seem to be nearly alone in that.
[EDIT] never got why in JS specifically. I get how in a language with proper support for it functional style can be nice. And it's fine in certain narrow cases of course—a little recursive function or simple list processing code, for instance, just not as a broad, principled pattern to apply to as much of a JS codebase as you can manage.
JavaScript does not make it easy. I'm having the same issue as I move towards more functional code in a large React codebase. I'm wanting to adopt more of these patterns but the JS language makes it difficult.
Specifically, JavaScript's lack of infix operators, pattern matching, type classes, and `do` notation make the code harder to read.
I'm evaluating Purescript now. I have concerns about developer ramp up time but other than that I like it.
Reason is more popular but it lacks the category theory centric rigor. The syntax also is not as nice as the Haskell style. I'd rather go with something that fits the FP model better than syntax that is more familiar.
Ramda is beautiful and works pretty well in isolation, until you start to use 3rd party code that doesn't work with it well, or you need to interface with non-ramda code in your system, or working with built-in DOM functions or any other number of reasons why you'd need to hop out of the ramda ecosystem.
Then I end up spending most of my time trying to figure out the best way to shove X, Y, and Z into the "ramda way", and often fucking stuff up in the process producing more harder to understand and track down bugs.
Then without perfect buy-in from the entire team, we end up with 3 different ways of filtering exceptions, people using native .map and mixing it with ramda's R.map, uncurrying, reordering, and re-currying functions, and places where the nice control flow is interrupted when it had to dive into 3rd party or native code.
It just ends up hurting every time I go for it, even though the functional style works amazingly in OCaml or Haskell or Rust.
I love ramda and i love fantasy-land but the problem is simply that likely 99% of your colleagues wouldnt understand it. That sucks but is the reality that i often encountered.
I used it also on greenfield projects where I then brought juniors in, and the learning curve for them was hard - at least..
So I kinda avoid it now, though using it taught me much. Even for my embedded C code.
I'm facing the same dilemma, do I go with what junior developers are going to be more familiar with or do I think of it from a 1st principles perspective and build in a way that makes the most sense from a pure engineering perspective. Unfortunately, I don't really see a way to scale past a certain point without these patterns.
It could possibly make sense in Typescript, depending on how good generics are.
But, generally, functional programming really shines when you have build-time type enforcement so you're not guessing as to whether things will work together.
In dynamically typed languages, all you know about a function is that its arguments have names and it returns a value. You can have some wonderful free-monads, but you're left to run the code, see it crash, and ponder stack traces or reason through why it failed. (Assuming it's nice enough to fail obviously.)
So you get lured in by how cool it is, but then the moment you use functional techniques beyond what you can fit in your head, you find your tooling gives you no support whatsoever.
A good example of this is automatic currying. In a statically typed language if you pass too few arguments to a function then you get an error message pointing right at the spot of the bug, assuming you have enough type annotations to guide the type inference (which you should).
On the other hand, in a dynamic language if you pass the wrong number of arguments to a curried function the error will only show up when the result of that is called, at which point the stack trace is useless.
Improving the error messages for things like this in the presence of dynamic typing is only possible with lots of help from the language runtime and doing it without ruining performance is still an open research question.
This looks like it'd be a massive pain to debug unexpected exceptions in the middle of a chain. Would you have to decompose the entire thing and step in and out of every step? Yuck.
You should never have unexpected exceptions if you embrace static typing and one of the many libraries that take this idea and do it better, for example `purify-ts`. It also comes with methods included for statements that might throw such as `JSON.parse`.
The entire purpose of this school of error handling is that there are no exceptions ever and unhandled errors are compiler errors.
It's weird in JS since exceptions to exist and you end up needing to build a library of primitives that swallow exceptions and return them as errors. I wouldn't do this in JS without tooling support because it's possible to miss them but in principle the case you describe should never be able to happen and that guarantee is enforced at the language level.
Yes! Personally I would argue a that this can end up being a little better in practice since errors don't interrupt your program's flow and there's not an implicit `if err jump` attached to every line but I suppose that's stylistic preference.
The 'Either' monad looks like a JS Promise without the asynchronicity. What he calls left/happy-path is 'resolved' in Promises, and right/sad-path is 'rejected'.
I understand that using Promises for error handling has some advantages in very unusual situations. But for the most part, I am happy about the greater readability and lower complexity that async/await offer.
> The 'Either' monad looks like a JS Promise without the asynchronicity.
Congratulations, you're discovering the general concept of monads :). By formalising that "looks like" you can write functions that operate generically on both, and even on other constructs that conform to the same interface.
> I understand that using Promises for error handling has some advantages in very unusual situations. But for the most part, I am happy about the greater readability and lower complexity that async/await offer.
It's always the edge cases that get you. Naively-implemented async/await do the right thing most of the time - and then they break down in exactly the most complicated cases where you really can't spare the mental capacity to deal with them doing something surprising.
If you implement async/await as lightweight syntax sugar over promises, then you get the best of both worlds: you have lightweight syntax for the simple cases, but can always fall back to the more explicit approach for the confusing cases. Similarly, if you implement throws/try/catch as lightweight syntax sugar over either, you can have the best of both worlds. Better still, you can implement a generic lightweight syntax sugar that's usable for any of these kinds of constructions: "do notation" in Haskell, or "for/yield" in Scala.
Yes, Promises are similar to monads but they don't obey all the monadic laws; as a result they don't compose as well.
This means that you're always "stuck in the weeds" of promises and can't write code that abstracts it away. Or that treats promises, observables, arrays, etc, exactly the same since they all have different "interfaces".
In that sense JS is more complicated because other languages don't make you learn different syntax and APIs for each one.
'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.
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 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 :)
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 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.
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).
Are people employing this pattern without static typing - curious whether people find the increased complexity in monad combination is worth the reduced complexity of coding for the happy path
I do this in ClojureScript (an actual functional language with macros to sugar this type of thing) and it's really not a good time. Do it if you're cornered like we were but we're moving away from it.
In a strict language (call-by-value semantics) the notion of a Monad makes no sense and degrades in merely a sum-type (an either-of algebraic data type).
There is absolutely nothing "monadic" in an ordinary either-of type with functions (which will make it an ordinary ADT).
I don't see the utility when JS already has an option type that's deeply integrated into the language: promises. The one advantage I could see in a hand-rolled option would be forgoing the extra event loop ticks used with promise resolution, but then you're limited to writing synchronous code.
In other languages, I normally use option types when things can fail in unexpected ways, which almost always means I/O.
> A referentially-transparent function will always give the same result for a given input. But we can’t say this about functions that throw exceptions.
Why not? Given the same invalid input, the function will throw the same exception. It seems to me like they only break referential transparency if you purposely don't consider exceptions to be a "result"
And if you don't, you can just take your throwing function, wrap it in a try-catch, and return errors. Voila, callers will have no idea it's really using exceptions inside.
Error handling in Rust works this way, and Rust doesn’t have exceptions. (It has panicking, but that’s expressly designed not to be caught except at the thread boundary.) For Rust, the pattern works really well. Since Rust 1.0, there have been additions to the language to improve ergonomics, like the ? postfix operator, which will return an error, or unwrap a successful value.
Fortunately, Rust didn’t go with Haskell’s foolish Either, Left and Right naming for its error handling type, but gave the enum and its variants meaningful names, obvious semantics and good methods to go with that, so that the Result type has two variants, Ok and Err.
I write plenty of Rust and plenty of JavaScript. I find that various patterns that work very well in Rust don’t work so well in JavaScript; sometimes because I care about performance (runtime and memory) and porting the pattern harms that; and sometimes because of how you can’t conveniently add arbitrary methods to a type like you can in Rust due to its trait system, and so you’d have to give up method call syntax in favour of a hodge-podge of function and method calls. (This becomes a problem with all dynamic languages I know of; this is perhaps most clearly seen in Python where functional programming constructs are second-class citizens, in part because the likes of map are a function rather than a method—well, and because of its lambda syntax.) Also because you’re often working on existing projects that use the normal JS way of doing things, rather than getting to build from scratch, which would more readily allow you extravagances like trying out error handling models unusual for the language.
What is foolish about the naming? It's a Bifunctor and while often used for error handling in programs it actually has nothing to do with error handling.
The naming, afaik, comes from the correspondence to the logical disjunction operator and so the constructors name the Left side of the disjunction and the Right side of the disjunction. Seems rather sensible to me.
If you use it for error handling, it takes a lot of mental energy to remember which side is the happy path and which side is the error path. Maybe (no pun intended), people should have used Either to build a Result type, but as it stands, raw Either is used everywhere by convention, creating confusion.
It would be nice if there was a standard type alias for Either which explicitly labeled good/bad values.
That being said, I don't think it takes that much energy to remember that the right is the good value. If you are comfortable with monads, just remember that monads must be parameterized over a single type, and for Either that will be the right type (because we must partially apply the type constructor with the left type to get it down to the correct form).
> That being said, I don't think it takes that much energy to remember that the right is the good value.
And then clever guys come and use left as the good value, because then want to retry on error (as you can see in this thread). Plus it takes quite some energy to discuss the, well, unfortunate naming.
I'm not sure which definition you're referring to, but every one I've seen you'd be hard-pressed to use left as the 'good' value. The type's definition favours 'right' for Functor/Applicative/Monad/etc instances.
There are zero implementations of Left being the Ok value, just this hypothetical put forward by another commenter.
edit: I should specify that I mean when Either is used as a 'Result' type. There are plenty of other use cases you can use Either outside of Ok/Err values.
Haskell's Either isn't intended to be used solely as Left = Err and Right = Ok. It's supposed to work as an abstract concept of "one thing or another", and the language designers have avoided choices that would canonicalize it that way, such as introducing a MonadFail (Either String) instance.
One example of why you would buck the trend would be to use of Left = Ok and Right = Err for a series of "retry" computations. Say you have a few functions of type Either a String, which all get progressively slower, but which work on progressively more advanced input. An example might be a series of regex engines, each of which run slower but handle more and more advanced lookahead syntax. You can then write something like:
This will return a Regex using a simple parser if it can, and if that function fails (by returning a Right value!) it will go onto the next function and try that.
Fun bit of history: Rust used to have Either. Eventually we added Result. At some point, we looked at all the code that existed (ahhh, the things you can do when you're young) and nobody used Either, only Result.
Either also lives on in the `futures` library, where it is used when you have multiple things that can happen in your asynchronous task and you want to stick to static types rather than returning a trait object.
It's not clever for its own sake. Haskell is rooted in math and Either is logical disjunction. Its semantics is precisely that. Error handling is a specific use-case that can be implemented with Either. Rust as a language is more pragmatic. In real-world industrial code the concept of Either is predominantly used for error handling and thus Rust named their data constructors aptly.
I am a big fan of returning arrays like [err, value] from async functions (kind of like Go, only reversed) which can be destructured on one line. Yes, it does end up with if/then statements but I find those cleaner than try/catch semantics.
That works well for simple programs, but if you write at scale, that style of programming is very error prone and tedious.
It introduces a lot of noise into the program and makes it harder to follow the logic.
If you don't lift yourself into higher levels of abstraction, it's much harder to focus on more difficult problems. For example, long division with Roman Numerals is more difficult than Arabic because there are simply more low level details to worry about.
Sure. The more things the programmer needs to worry about and explicitly state, then there more pathways there are for error.
Using assembly language vs a higher level language illustrates this. There are certain classes of errors that are eliminated by the language / compiler / runtime. Examples could be garbage collection preventing you from have page segmentation faults, or accidentally modifying the stack pointer and destroying the return address.
Rich types like Maybe / Either offload the exception handling and allow you to write your code for just the happy path while still having the program fully support the unhappy path.
Compilers with pattern matching can give you errors when you have not handled all possible inputs to a function (total function). If you just have the "if err != nil" or null-check style code then it's easy to miss them.
By tedious, if you constantly have to type the same thing over and over ("if err != nil") then it takes away time and mental energy from focusing on the core problem domain.
Being able to express guard conditions to a function allow your functions to be simpler and easier to understand.
It basically allow comes down to abstraction and letting the compiler / language do things that it is good at and freeing the programmer from having to worry about every single permutation. It's just not feasible for the human mind to see every possible branch of a program.
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.
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.
Yes, 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.
Yes, 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.
People 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.
"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.)
> In Haskell do notation, every single "<-" is actually an entirely new function, nested inside the previous one ... 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.
This "nested functions" pattern is precisely what promises/futures/async-await does. It's just a different way of expressing the same things - "promises" are just continuations, which are as general as do notation itself.
> 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.
I just updated the links to FP-TS and Sanctuary... have you checked those out? They seem to tick all the boxes as far as I can tell.
I really like a lot of functional programming techniques in JS, but the Left/Right style of error handling just always seems messy and unreadable to me.
That’s an impressive amount of work to carry an error state around, and check before each step if there is an error set.
I feel like this is a trivially simple idea which has been contorted by poor nomenclature and lacking the proper syntactic sugar, which results in significantly more cognitive load than should be necessary to achieve the desired result.
There’s all manner of creative ways to solve problems in code but if they aren’t idiomatic then, IMHO, they aren’t applicable. Code should be first and foremost understandable by humans and only incidentally for compilers.
There's a point where long boilerplate becomes unmaintainable by humans.
Approaches like this remove boilerplate in intuitive ways; look at the amount of code in the `Left` and `Right` implementations. They make code more understandable. C needs not to be the idiom for Javascript, despite syntactic borrowings made in 1994.
This approach doesn't remove boilerplate, the initial version is 8 lines, the final version is 6 lines. Except the final version has a lot of code hidden in helper methods, making it impossible to understand at first glance without reading those helper methods. (LiftA2, chain, Left, Right, Lodash helper methods).
The initial version is also very clear, we have a set of method calls that execute sequentially, and if any of them fail, we go to the catch block and call showError. This indicates that all functions in the try block can throw an error. The final version goes through a lot more trouble yet I don't get any extra information by reading the code. Now I don't even know which functions are error prone because everything is a mix of weird functions like map, chain, and liftA2. The second version also seems to promote more cognitive load. Try-Catch is simple, it doesn't require any brain cells to understand. This approach required an entire blog post. It seems better to dedicate mental energy to more important decisions rather than a simple function that processes CSVs.
I think I understand the benefits of functional programming when one is writing a compiler, but this seems to be a misuse of functional techniques.
The article's example is a toy example. In a real-world example of the same approach, like this piece of highly non-idiomatic Python that still reads much like plain English [1].
> Try-Catch is simple, it doesn't require any brain cells to understand
...because you have already spent the effort to learn it.
Equally, monadic code can be quite compact and simple, once you have have spent the effort to learn a new approach.
Also, a catch clause does not give you an idea what data cause the crash, only what line. The Either-based approach allows every place to report all relevant local info; the linked code has complete error reporting.
Everything is non-idiomatic until it is. If you write JS and use Promises and `await`, you already use monadic behavior and an analog of Haskell's "do notation". Some might think that callbacks are more intuitive, though.
I think this is something where it works well with language support (like Rust), but it seems really unidiomatic in JavaScript, and so it ends up hurting readability more than helping it. In a team context you would have to make a code style decision to handle things this way.
You would then have to teach every developer who you onboard this novel way of doing things.
I feel idiomatic code is really the best code, even if it's not always the most elegant way to achieve something it excels in practical ways. This is definitely one of the benefits of using a framework. Not every language has strong idioms for all uses, but you can often find them in a framework.
There's no single definition of "best"; your is implicit, but what specifically do you imply?
Straightforward, idiomatic imperative code can be "best" if you have to maintain a legacy app and can hire developers with little experience and desire to learn.
Code which is less like Basic can be "best" if it allows your team to move twice as fast, and have 10x as short a backlog of defects to fix. It may take a more expensive team, and some onboarding time for new members, of course.
> Code which is less like Basic can be "best" if it allows your team to move twice as fast, and have 10x as short a backlog of defects to fix. It may take a more expensive team, and some onboarding time for new members, of course.
This is true in theory but it’s famously hard to measure, having both very strong religious beliefs and notorious confounds. For example, is any particular anecdote telling you that a niche language like Lisp or Haskell actually makes people more productive or just that you’re filtering for developers who would be above average in any language?
Actually measuring these sorts of claims scientifically is expensive enough that it’s rarely attempted, but that rarely causes people to taper the degree of certainty people express. That doesn’t mean that new languages are bad but I think Fred Brooks was right to recommend skepticism of big claims rather than more incremental improvements.
I wasn't really clear sorry. I am really advocating for idiomatic code at the implementation level. Of course your architecture will be novel, and it's there that you build efficient and easy to use systems. You want people to be focusing on that, not being distracted by arbitrarily different unidiomatic details.
The notion of "idiomatic" changes over time. I've seen quite a change in Java during 20 years I use it. I've seen quite a change in JS frontend approaches for last ~5 years, since the wide adoption of React.
Expecting any improvement to come with a flat learning curve is unrealistic at any job, and especially so at programming.
Monads like this are very problematic in the JavaScript world. I say this as an F# programmer, and our community is not unfamiliar with Railway Oriented Programming (https://fsharpforfunandprofit.com/rop/)
Unlike F# or any other decent functional language, JavaScript doesn't have very good constructs to guide and ensure the programmer is using these constructs properly, let alone using alongside with existing error propagating constructs.
What happens when you mix these with promises, or RxJs, or something else?
My advice would to be avoid using constructs like this for general purpose error handling and use tried and true constructs for Exceptions, which should be exceptional.
Something like this isn't entirely useless, sometimes its useful to encode errors in an abstract type when the errors become an integral part of the problem domain. Sometimes errors aren't exceptional and you need a way to deal with them that's somewhat performant. Even if that's the case, you need to be careful with constructs like this because it's easy to use them wrong, which might lead to scenarios where errors aren't properly thrown and propagated.
I'm not saying to never use them. I'm just heeding you should tread carefully.
Exceptions are for exceptional stuff, that is, something you do not normally expect. Their purpose is cleanup after a non-catastrophic failure.
OTOH you totally expect I/O errors and parsing errors while reading a CSV file. You totally expect a mismatch when matching a regexp. Using exceptions here is discouraged even by Martin Fowler, of all luminaries [1].
>we use the term exception for expected but irregular situations at runtime and the term error for mistakes in the running program that can be resolved only by fixing the program. https://wiki.haskell.org/Error_vs._Exception
If you start a thread and it gets killed by OS, it's an exception. You cannot predict when or where it happens, and normally it won't happen; there is no error in your code that leads to this. You can still try to gracefully react on a thread death situation in your code by catching an exception.
OTOH if you wrote a non-total function and it fails to proceed given some arguments, this is an error. You should not try to somehow continue if it happens; you should fix your program.
> If you start a thread and it gets killed by OS, it's an exception. You cannot predict when or where it happens, and normally it won't happen; there is no error in your code that leads to this. You can still try to gracefully react on a thread death situation in your code by catching an exception.
I don't see how this is different from when a file that's supposed to exist doesn't. I guess it's a matter of degrees.
not finding the requested record is a perfect case for an Either, or something that is potentially (legitimately) null. Thats not an exception generally.
That sounds like the perfect place for a single if (val === null) { handle that very predicable case and return }
That whole example in the linked article displays coding style that surely makes sense but I can't imagine having that as a theme in my codebase. It's fringe and not native to the language, which makes onboarding others to that codebase a pain.
I dont disagree - where things like either, maybe or result really shine is when its baked into the stdlib _and_ there is machinery built into the language to make using them easy (like pattern matching, and them being in scope by default). I have used and written several libraries trying to make them work in js/ts and while they are beneficial around my custom domain specific code, you still spend a bunch of time wrapping things coming from libraries or other code you dont have control over.
But the benefit of them over a check against null is that you can write a pipeline of functions that dont have to handle those cases and you know that its always safe to do so. This other article by the same author as the OP I think illustrates it well https://jrsinclair.com/articles/2016/marvellously-mysterious...
Unlike F# or any other decent functional language, JavaScript doesn't have very good constructs to guide and ensure the programmer is using these constructs properly, let alone using alongside with existing error propagating constructs.
Javascript has at least two mature type checkers you can use that will let you use these constructs just as safely as in F#.
What happens when you mix these with promises
In my either library I added rightMapThen and leftMapThen, and called it a day. worked absolutely fine. I think I also have rightFlatMapThen and leftFlatMapThen. Works flawlessly, and I have everything I enjoyed about the equivalent in F# (where I first encountered the concept).
It's not even accidentally bad. They went through a significant amount of effort to make it a great deal worse than simple black text on a white background.
Another thing to think about is that exceptions make our code impure. [...] A referentially-transparent function will always give the same result for a given input. But we can’t say this about functions that throw exceptions. At any moment, they might throw an exception instead of returning a value.
That’s just incorrect. If it’s a pure function, it would always throw exactly the same exception given the same input. It would still be referentially transparent.
Sure, it’s a pure function with two exit paths, which is more of a hassle and possibly more error-prone to deal with. But it can still be pure, so you can still get the benefits of pure (dynamically-typed) functional programming.
Edit: to make sure we’re on the same page, I interpret “pure” as no side-effects and no non-determinism, so a call with the same arguments always has exactly the same result.
What side-effect does “catch” have? Something that’s visible externally, visible to the caller of the function doing the catch.
I suppose if you catch an exception and pull out the stack trace, that gives you some extra information about the caller. Even that doesn’t make you impure, though, if you treat the call stack as additional input.
"A referentially-transparent function will always give the same result for a given input. But we can’t say this about functions that throw exceptions. At any moment, they might throw an exception instead of returning a value."
That's the premise of the article and it is demonstrably false. You throw exceptions in a reproducible manner based on input, not a RNG!
and so on.. Except with zero. With zero you get an exception, however nothing in function signature mentions about such result. You only expect a double
In Either case - it solves the problem, because the signature would be like this:
function oneDividedByX(x: Double): Either[ComputingError, Double]
As for throwing exceptions - it can be done in pure way, but catching exceptions cannot
I've always thought that the Left and Right naming in the Either type was a poor choice in Haskell. Left and Right don't convey any semantics at all and I constantly need to spend mental energy on remembering which is the Ok and which is the Error (speaking in Rust Result terms). Sad to see that people are translating these naming mistakes into JavaScript.
The underlying error handling methodology however is awesome and I miss its guarantees and composability in my daily C++ work (yes I know, there's Boost Outcome, but they stripped it of everything useful before including it in boost, as far as I'm concerned).
I think the point is there is no inherent meaning. It is just two possible results. Could be an error, could just be another likely path. Will only be one of them, though.
Left only shortcuts because of the implementation of the map function. There could just as easily be mapLeft and mapRight functions instead, allowing you to choose the shortcut behaviour.
I don't think there's any documentation on Haskell's Either monad that doesn't explain that Right=Good even if it's only by convention. But its functions also has right affinity, so Right and Left cannot have equal weights. You have to decide why this result is in the right/default slot over the other one.
TFA had so many parentheticals and explanatory comments on this that it would have been shorter if it used "Sad" and "Happy" instead. Of course, if brevity had been a goal there would also have been some inheritance...
I never thought about it before, but I wonder if there is a connection? - along the lines of "correct" and "just" (in the sense of ethically correct) being the opposite of bad / evil? Some people here [1] think so, but "right" appears to have Germanic roots (but then again, perhaps the idea of right/good and left/bad may predate the Romans, and maybe goes back to the time of proto- Indo-European, or even further.)
"Right" also has the meaning of "upright" (as in right angle) or, archaically, as "straight", and the normal processing path might be regarded as the straight one in the railway metaphor.
> I constantly need to spend mental energy on remembering which is the Ok and which is the Error
Right is actually a synonym for Ok.. besides, Either is useful for _much_ more than error handling. That is exactly why I dislike Error for the left side; often it's just not an error.
A mistake easily corrected. They do convey the exact meaning well. It's the analogy that appeals to your intuition which is wrong.
Either a b === a || b
Where `===` means equivalent to. This means that a value of the type can hold either the `a` or the `b`. In order to construct a value of this type you used the `Left` function to construct a value contain the value on the left side of the operator and Right for the other side.
They have no "semantics" otherwise. You could call them "Jib" and "Jab," and it'd amount to the same thing. Simply because you ascribe a particular meaning doesn't change their nature. Hence why Left and Right.
Now I do recommend, for readability's sake, ascribing an alias to this type if it aids your understanding of the code when you use it in a particular way that gives your code a certain meaning!
I think they're saying he would prefer some semantics - like one is the preferred or normal case, and the other is an error or unexpected case.
They might convey the exact meaning well, but they (and I) would prefer a type with slightly different semantics that better remind us what each side actually represents.
Did you catch about what OP was saying though? Instead of Normal and Unexpected they are recommending you write something like:
const ChargeSuccess = Left
const ChargeDeclined = Right
And then use that language in your code. Those words tell you something about the semantics of the code being wrapped. “Normal” and “Error” don’t actually mean much.
Also, you might want to someday write:
const NormalDelivery = Left
const FastTrack = Right
And that wouldn’t make much sense if fast track is a so-called error condition.
I did see that. This is a common pattern in many functional languages, and (for example) Scala has the same issue with Either. It's a common enough complaint that there are numerous right-biased utility versions in the wild that make it more ergonomic.
Having a type that is right biased doesn't mean it can't also implement the more generic interface. If your needs change you can always go back to using the less opinionated version with little breakage.
Rust's is notable in that they have both Either and Result. I think there's value in that, even if it is mainly for ergonomics and communication.
Also, aliases are fine, I could just see them being duplicated a lot. Probably not my first choice.
> Rust's is notable in that they have both Either and Result. I think there's value in that, even if it is mainly for ergonomics and communication.
either isn't in std though, it's a third party crate. Personally, I think the core data structure should be as generic as possible. The reality is that if the Result type was named appropriately, the Either crate would be redundant.
What enum? We're talking about a specific data type, it doesn't make any sense to say 'an enum' is the generic version of Result.
I understand why they chose Result instead of Either. I just disagree with it; what you end up with is 2 data structures with the exact same structure and implementations but that represent different things. If you have Either in std, then Result is redundant, but not the other way around.
> What Result? Result is a generic not a specific type. It's essentially a shorthand instead of manually writing out the enum yourself.
No, it's not 'a generic', but it has generic type parameters. An enum can be any number of variants with any number of type parameters.
A result is:
data Result a b = Err a | Ok b
(or in Rust)
enum Result<T, E> { Err(E), Ok(T) }
> It's essentially a shorthand instead of manually writing out the enum yourself.
I'm not sure which enum you're referring to. You do realise an enum can have any number of variants, not just 2? I could define a sum type with 16 variants. 'enum' is just Rusts keyword for defining sum types.
> Result has semantic meaning that's lost with Either.
> You do realise an enum can have any number of variants, not just 2?
Yes, that's what makes it more generic than Result. Indeed that's the only practical difference except for a few helper functions and a #[must_use] (which only makes sense for Result, not for a more general Either type).
>> Result has semantic meaning that's lost with Either.
>Which is what?
Seriously? Error checking! That's the whole point of it. One side has meaning over the other; it has weight. The "left" is the result you want. The "right" is when something gone wrong.
There definitely are more semantics than this in the monad instance for Either, which treats Left as "failure" (via early exit in a comprehension) and Right as "success". The docs call out the convention as well.
Or, to put that another way, Either is "cons" for types, and you can build up a sum type as a type-level linked list of types by repeatedly "cons"ing (Either-ing) types together: (a || b || c || ...). The functional-programming traditionalist would call the type-level "slots" of an Either its "CAR type" and "CDR type".
But, since there's no abstraction that assumes that such a structure of sum-types is shaped like a proper list, there's nothing stopping you from building any arbitrary binary tree of types using Either. And so, like any other binary tree, the two branches get called Left and Right.
I remember it by thinking that Left (error state) has to be handled first. This forces developers to always think of the erroneous state first and foremost. And then define the Right (success state).
On the other hand, it may be easier to forget the error state if it were the second argument. As is often the case with Promises and JS developers.
To my mind the problem isn't so much having Either as using it as Result - which I agree was unfortunate. In the same way that fst and snd don't tell me much about a tuple.
At least there's a pun (right vs. left, right vs. wrong), which is probably why the pattern stuck, but I don't think that's actually sufficient justification.
They are named like this because you can use it for more than just errors, it just so happens that it's a good fit for errors. In fact, I wish Rust had named it Either, instead it's called Result (with Ok/Err variants). This is great for errors, but it leads to the creation of (https://crates.io/crates/either) because there are scenarios where you want Either and the Err/Ok name is too presumptuous.
A good way to remember which variant represents success in the case you're talking about an error is that Right is the "right" variant, Left is the "wrong" one.
Yes it's terrible naming. The more abstract the naming, the harder it is to reason about.
Plus, JS/TS already have proper union types without a need for a construct like Either.
A better naming for 99% of cases is (like found in Elm)
Plus, JS/TS already have proper union types without a need for a construct like Either.
I disagree - what if on success it returns a string representing some data, and on failure it returns a string detailing the error? A `string | string` return type is not very useful, while an `Either<string, string>` is IMO.
I did this a ton when building some mixed-sync/async stuff in TypeScript, and tend to use it (or something like it, depending on the language) for any situation that looks sufficiently similar. What are the downsides to this? Why isn't it more common? (or maybe it is, and I'm just not reading the right code to see it)
249 comments
[ 2.3 ms ] story [ 261 ms ] threadThis actually reminds me a LOT of making websites around 2006 - 2012.
This is where browsers' reader modes really help. Is Chrome the only one now that doesn't natively support reader mode? Reader mode in Firefox made this much easier to read.
I actually just didn't notice any difference in legibility and rather focused on the neat effect of the brushmarks around the div enclosing the code.
I kind of figured people would complain about this though---its an artistic sight, and not everyone is open to form over function.
Actually, I think it is intended to look like mechanical typewriter output.
If you want to exit the program because the error can't be handled (similar to an unhandled exception), rust has a different concept for it, panics, and any Result can be turned into a panic via `unwrap()`.
IMO both are important: panics represent unrecoverable errors, while Results can be acted upon (for instance, if read() returns Err(EINTR), you'll want to retry the read)
[0]: Granted, you need to write some boilerplate to get the backtrace on error creation.
[1]: You can cast all errors to `Box<dyn Error>`, a heap-allocated dynamically dispatched Error type.
> I never understood the love for Either-based error handling in Haskell and other languages.
What does a function that doesn't error look like under try-catch? How do you tell it apart from a function that can error.
It's the same argument as null/Optional. Yes, Optional behaves the same as a nullable value; the point is that languages with Optional allow you to have values that aren't nullable.
By the empty throws clause.
Three major problems: they interact inconsistently with generics, you can no longer take a function's result and store it as a value, and you still can't tell whether a function might throw by looking at the point where it's called.
A good version of exception specifications would be lightweight syntax sugar over either (in the same way that a good version of async/await is a lightweight syntax sugar over futures/promises). But I'm not aware of any language implementing that; I guess once you have working Either there's not enough value in providing an alternative syntax for it.
For system failures it's ok because the only handling you're going to do is retry at high level (and so even languages with an Either-oriented style tend to have some form of "panics" or exceptions for that kind of failure). But it can be a real problem when that also happens for "normal failure" paths like bad user input where you want to actually have business logic that handles and perhaps recovers from the failure.
If a method can fail on IO that's different than if it cannot fail. And the surrounding code has to account for that.
So, you might think that the methods are interchangeable if the error type is not present in the signature but this is actually wrong. Different failures have to be handled differently.
(I assume that you are /not/ talking about valid error cases in the problem domain that also might have to be handled)
That's exactly what using either instead of try/catch does! try/catch are magic language keywords that create invisible, surprising control flow. Either is plain old code written in the normal language, with functions that follow the normal rules.
Try/catch makes it _incredibly_ clear what you're trying to do. Kids who graduate college and get their first real job understand what a try/catch does. A random contributor to an OSS project knows what try/catch does.
Meanwhile, I'd be willing to bet that not even 10% of programmers could actually explain what a monad is.
I'm sure there is a place for this elegant error handling, but in most codebases it seems like a pretty big complexification for not all that much benefit. Sure, the code might even be "more correct" (whatever that means), but if Samantha the intern can't pick it up rather quickly, it probably isn't all that well suited for mainstream usage.
The for loop is indeed a magic keyword, though it's less surprising/magic than try/catch; most of what for does could be done by a plain old function.
> Meanwhile, I'd be willing to bet that not even 10% of programmers could actually explain what a monad is.
Don't try to generalize prematurely. Considering Either on its own, it's simpler than try/catch and can replace their use cases. If you'd started with Either, try/catch would seem like the overcomplicated solution in search of a problem that it is.
But we didn't start with Either, which IMO is an incredibly important distinction.
Bolting things like this onto the language after the fact isn't the same as having first class support by default (like in say, Haskell)
It's taking the logical disjunction operator, `||` in many languages, and sticking it in a data structure. You can now pass it around like a normal JS value and combine it with other such values.
The benefit of this over exceptions is that for unexceptional situations you don't end up throwing away the context of your computation if something takes the "bad path." Instead of jumping to the exception handler and losing all of your data your program can handle the situation at the site of the error where it has the most context to solve the issue.
TFA wasn't advocating abandoning try/catch -- it was suggesting that for non-exceptional cases it will make your code cleaner.
To be fair, I don't think you need to really understand what monad is to understand Either.
Personally I just try not to throw exceptions when compensation/recovery is expected. If something should be handled by the caller put it in the return type, if it’s really fatal give up and log a stack trace.
Agreed. Every keyword adds complexity to the language; the fewer your language needs, the better. If your language has first-class functions and polymorphism (and any serious language does these days), there's no need for special-case control flow keywords; better to have a design like e.g. Smalltalk, where if/while/... are just normal functions.
> The control flow that try/catch creates has never been surprising or invisible in my experience.
One of the biggest production bugs I saw happened because of removing an unused variable (the function looked correct, but actually the unused variable right at the top of the function was throwing an exception; the fallback code path was correct, but the function itself was implemented wrong). It's the same problem as https://glyph.twistedmatrix.com/2014/02/unyielding.html - you can't tell which function calls might throw by looking at them, and most of them don't, but some of them do. And on top of that you have the goto-like "action at a distance": starting from a given catch there's no way to find the corresponding throw (or vice versa). You can't even tell when a catch is completely unused.
Easily refuted observing that you could strip languages of many of their keywords (for example replacing for and while with if and goto), and you'd end up with less readable code. Keywords are often added to make languages simpler, at least because they declare the intention of the developer.
Otherwise, Brainfuck would be the least complex language to write code in.
Better, where possible, to implement general-purpose reusable functionality in libraries written in ordinary code in the language. That's a win-win approach: reusable library code can simplify codebases written in the language, but since it's plain old code that follows the normal rules of the language, a library doesn't add language complexity that the developer is forced to keep in mind.
Fundamentally, the key to making codebases in your language understandable is to be compositional: make sure that the developer can easily understand the combination of a and b if they understand a and b separately. Library functions do that, because you can understand how code that calls a library function behaves without needing to understand what the library function does. But language features like throw/catch don't do that: you need to understand what throw does if you are to understand code that calls a function that throws, even if the code you're trying to use doesn't throw itself.
I'm surprised people believe this. If you added multiple returns to every function and manually propagated every error you'd get exactly the same thing as try/catch. It's existence comes from not wanting to do all this pointless boilerplate and instead having the compiler do it for you.
You'd get the same control flow, but it would be visible in your code instead of invisible/magic.
> It's existence comes from not wanting to do all this pointless boilerplate and instead having the compiler do it for you.
Right, but making it be completely invisible goes too far - it's the same problem as AOP.
Either gives you the best of both worlds - minimal boilerplate, but you can still see what's going on.
This pattern is not more complex than try/catch. It's just JavaScript lack constructs to make the writing much less verbose. (we could abuse async/await but it's tricky).
Try/catch is an extra language construct which is confusing - it has dynamic semantics just like JavaScript's weird 'this' behavior. And it's leaky just like null - when you forgot to handle something, the whole thread explodes. Besides, it's not an expression, which means it's harder to refactor or return with function, passing as an argument, etc. On the contrary, Either would be just fine everywhere, just typical functions and values.
Another use case is when you want to collect the errors along the road, instead of failing fast behavior, in this case, Either would be easier than try/catch.
I'm currently working in a codebase that pulled a ton of this functional style into javascript and it's overwhelmingly complicated and difficult to follow even for me who has a bit of "functional" programming experience (mostly ocaml).
Maybe I'm just missing something or maybe i'm just dense, but I haven't ever personally seen a situation where the complexity of setting up and using this functional style in javascript has ever been more readable or maintainable than the equivalent "traditional" imperative programming. It's one thing if the language or ecosystem kind of enforces this and gives you a standard set of tools to work from or helps with this style via language constructs, but javascript is not that language.
For the exact example in the article, both the try/catch version and the "nullcheck" version seem much simpler to understand (even if they are admittedly a bit "ugly" looking). And while I get that the author is using a simplistic example to help teach, it really doesn't seem like a more complex example would change things.
Forcing sum types onto a language that does not support them is bound to end in awkwardness and reduced readability without providing the real benefits of increased correctness.
This is a great example. std::option from C++ is another one.
If someone did this at work, I'd wonder why they are trying to add so much complexity to something that is rather simple and already well understood by most.
It would be hard to convince me to sign off on a code review where this was added tbh. I'd love to hear how/why I'm wrong here though.
Maybe there are better use cases for it? But in my case, the major thing I care about is that code is simple and readable.
That said, I work with quite a few juniors. You should absolutely not use this in production.
When I've felt the best writing JS was back when it was a much worse language (which is saying something) and I wrote it about as close to C as I could manage. But then I've always thought the prototypal object model is a giant footgun with little legitimate purpose and any "clever" use of it's simply a bad idea, so maybe I'm just inflexible. I also think modern classes are entirely fine, even good, for Reactjs, and adding more patterns just to be more "functional" (but not really—look under the hood) is a very bad idea, but I seem to be nearly alone in that.
[EDIT] never got why in JS specifically. I get how in a language with proper support for it functional style can be nice. And it's fine in certain narrow cases of course—a little recursive function or simple list processing code, for instance, just not as a broad, principled pattern to apply to as much of a JS codebase as you can manage.
Specifically, JavaScript's lack of infix operators, pattern matching, type classes, and `do` notation make the code harder to read.
Reason is more popular but it lacks the category theory centric rigor. The syntax also is not as nice as the Haskell style. I'd rather go with something that fits the FP model better than syntax that is more familiar.
Ramda is beautiful and works pretty well in isolation, until you start to use 3rd party code that doesn't work with it well, or you need to interface with non-ramda code in your system, or working with built-in DOM functions or any other number of reasons why you'd need to hop out of the ramda ecosystem.
Then I end up spending most of my time trying to figure out the best way to shove X, Y, and Z into the "ramda way", and often fucking stuff up in the process producing more harder to understand and track down bugs.
Then without perfect buy-in from the entire team, we end up with 3 different ways of filtering exceptions, people using native .map and mixing it with ramda's R.map, uncurrying, reordering, and re-currying functions, and places where the nice control flow is interrupted when it had to dive into 3rd party or native code.
It just ends up hurting every time I go for it, even though the functional style works amazingly in OCaml or Haskell or Rust.
I used it also on greenfield projects where I then brought juniors in, and the learning curve for them was hard - at least..
So I kinda avoid it now, though using it taught me much. Even for my embedded C code.
But, generally, functional programming really shines when you have build-time type enforcement so you're not guessing as to whether things will work together.
In dynamically typed languages, all you know about a function is that its arguments have names and it returns a value. You can have some wonderful free-monads, but you're left to run the code, see it crash, and ponder stack traces or reason through why it failed. (Assuming it's nice enough to fail obviously.)
So you get lured in by how cool it is, but then the moment you use functional techniques beyond what you can fit in your head, you find your tooling gives you no support whatsoever.
On the other hand, in a dynamic language if you pass the wrong number of arguments to a curried function the error will only show up when the result of that is called, at which point the stack trace is useless.
Improving the error messages for things like this in the presence of dynamic typing is only possible with lots of help from the language runtime and doing it without ruining performance is still an open research question.
It's weird in JS since exceptions to exist and you end up needing to build a library of primitives that swallow exceptions and return them as errors. I wouldn't do this in JS without tooling support because it's possible to miss them but in principle the case you describe should never be able to happen and that guarantee is enforced at the language level.
Or the cases with Rust’s assertions which kill the program?
The chance you’d need to rebuild a huge amount of software for that.
I understand that using Promises for error handling has some advantages in very unusual situations. But for the most part, I am happy about the greater readability and lower complexity that async/await offer.
Congratulations, you're discovering the general concept of monads :). By formalising that "looks like" you can write functions that operate generically on both, and even on other constructs that conform to the same interface.
> I understand that using Promises for error handling has some advantages in very unusual situations. But for the most part, I am happy about the greater readability and lower complexity that async/await offer.
It's always the edge cases that get you. Naively-implemented async/await do the right thing most of the time - and then they break down in exactly the most complicated cases where you really can't spare the mental capacity to deal with them doing something surprising.
If you implement async/await as lightweight syntax sugar over promises, then you get the best of both worlds: you have lightweight syntax for the simple cases, but can always fall back to the more explicit approach for the confusing cases. Similarly, if you implement throws/try/catch as lightweight syntax sugar over either, you can have the best of both worlds. Better still, you can implement a generic lightweight syntax sugar that's usable for any of these kinds of constructions: "do notation" in Haskell, or "for/yield" in Scala.
This means that you're always "stuck in the weeds" of promises and can't write code that abstracts it away. Or that treats promises, observables, arrays, etc, exactly the same since they all have different "interfaces".
In that sense JS is more complicated because other languages don't make you learn different syntax and APIs for each one.
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.
to mock TDD a bit: Paranoid Driven Development
But that's still a shower thought theory you know :)
- 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)
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.
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.
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).
There is absolutely nothing "monadic" in an ordinary either-of type with functions (which will make it an ordinary ADT).
In other languages, I normally use option types when things can fail in unexpected ways, which almost always means I/O.
Why not? Given the same invalid input, the function will throw the same exception. It seems to me like they only break referential transparency if you purposely don't consider exceptions to be a "result"
Fortunately, Rust didn’t go with Haskell’s foolish Either, Left and Right naming for its error handling type, but gave the enum and its variants meaningful names, obvious semantics and good methods to go with that, so that the Result type has two variants, Ok and Err.
I write plenty of Rust and plenty of JavaScript. I find that various patterns that work very well in Rust don’t work so well in JavaScript; sometimes because I care about performance (runtime and memory) and porting the pattern harms that; and sometimes because of how you can’t conveniently add arbitrary methods to a type like you can in Rust due to its trait system, and so you’d have to give up method call syntax in favour of a hodge-podge of function and method calls. (This becomes a problem with all dynamic languages I know of; this is perhaps most clearly seen in Python where functional programming constructs are second-class citizens, in part because the likes of map are a function rather than a method—well, and because of its lambda syntax.) Also because you’re often working on existing projects that use the normal JS way of doing things, rather than getting to build from scratch, which would more readily allow you extravagances like trying out error handling models unusual for the language.
The naming, afaik, comes from the correspondence to the logical disjunction operator and so the constructors name the Left side of the disjunction and the Right side of the disjunction. Seems rather sensible to me.
That being said, I don't think it takes that much energy to remember that the right is the good value. If you are comfortable with monads, just remember that monads must be parameterized over a single type, and for Either that will be the right type (because we must partially apply the type constructor with the left type to get it down to the correct form).
And then clever guys come and use left as the good value, because then want to retry on error (as you can see in this thread). Plus it takes quite some energy to discuss the, well, unfortunate naming.
edit: I should specify that I mean when Either is used as a 'Result' type. There are plenty of other use cases you can use Either outside of Ok/Err values.
One example of why you would buck the trend would be to use of Left = Ok and Right = Err for a series of "retry" computations. Say you have a few functions of type Either a String, which all get progressively slower, but which work on progressively more advanced input. An example might be a series of regex engines, each of which run slower but handle more and more advanced lookahead syntax. You can then write something like:
This will return a Regex using a simple parser if it can, and if that function fails (by returning a Right value!) it will go onto the next function and try that.Today, either lives on as a package: https://crates.io/crates/either It gets a lot of downloads because it is actually used by a few popular libraries: https://crates.io/crates/either/reverse_dependencies
https://docs.rs/futures/0.1/futures/future/enum.Either.html
It introduces a lot of noise into the program and makes it harder to follow the logic.
If you don't lift yourself into higher levels of abstraction, it's much harder to focus on more difficult problems. For example, long division with Roman Numerals is more difficult than Arabic because there are simply more low level details to worry about.
Using assembly language vs a higher level language illustrates this. There are certain classes of errors that are eliminated by the language / compiler / runtime. Examples could be garbage collection preventing you from have page segmentation faults, or accidentally modifying the stack pointer and destroying the return address.
Rich types like Maybe / Either offload the exception handling and allow you to write your code for just the happy path while still having the program fully support the unhappy path.
Compilers with pattern matching can give you errors when you have not handled all possible inputs to a function (total function). If you just have the "if err != nil" or null-check style code then it's easy to miss them.
By tedious, if you constantly have to type the same thing over and over ("if err != nil") then it takes away time and mental energy from focusing on the core problem domain.
Being able to express guard conditions to a function allow your functions to be simpler and easier to understand.
It basically allow comes down to abstraction and letting the compiler / language do things that it is good at and freeing the programmer from having to worry about every single permutation. It's just not feasible for the human mind to see every possible branch of a program.
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/ [1] https://sanctuary.js.org/ [2] https://github.com/fantasyland/fantasy-land
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.
When the compiler does require you to explicitly acknowledge exceptions (Java), people hate it.
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.
https://implementsblog.com/2015/04/07/better-checked-excepti...
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.
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.
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:
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.)
This "nested functions" pattern is precisely what promises/futures/async-await does. It's just a different way of expressing the same things - "promises" are just continuations, which are as general as do notation itself.
I just updated the links to FP-TS and Sanctuary... have you checked those out? They seem to tick all the boxes as far as I can tell.
I feel like this is a trivially simple idea which has been contorted by poor nomenclature and lacking the proper syntactic sugar, which results in significantly more cognitive load than should be necessary to achieve the desired result.
Kind of like that sentence actually.
Approaches like this remove boilerplate in intuitive ways; look at the amount of code in the `Left` and `Right` implementations. They make code more understandable. C needs not to be the idiom for Javascript, despite syntactic borrowings made in 1994.
The initial version is also very clear, we have a set of method calls that execute sequentially, and if any of them fail, we go to the catch block and call showError. This indicates that all functions in the try block can throw an error. The final version goes through a lot more trouble yet I don't get any extra information by reading the code. Now I don't even know which functions are error prone because everything is a mix of weird functions like map, chain, and liftA2. The second version also seems to promote more cognitive load. Try-Catch is simple, it doesn't require any brain cells to understand. This approach required an entire blog post. It seems better to dedicate mental energy to more important decisions rather than a simple function that processes CSVs.
I think I understand the benefits of functional programming when one is writing a compiler, but this seems to be a misuse of functional techniques.
> Try-Catch is simple, it doesn't require any brain cells to understand
...because you have already spent the effort to learn it.
Equally, monadic code can be quite compact and simple, once you have have spent the effort to learn a new approach.
Also, a catch clause does not give you an idea what data cause the crash, only what line. The Either-based approach allows every place to report all relevant local info; the linked code has complete error reporting.
Everything is non-idiomatic until it is. If you write JS and use Promises and `await`, you already use monadic behavior and an analog of Haskell's "do notation". Some might think that callbacks are more intuitive, though.
[1]: https://pastebin.com/wY8ZC0mP
I feel idiomatic code is really the best code, even if it's not always the most elegant way to achieve something it excels in practical ways. This is definitely one of the benefits of using a framework. Not every language has strong idioms for all uses, but you can often find them in a framework.
Straightforward, idiomatic imperative code can be "best" if you have to maintain a legacy app and can hire developers with little experience and desire to learn.
Code which is less like Basic can be "best" if it allows your team to move twice as fast, and have 10x as short a backlog of defects to fix. It may take a more expensive team, and some onboarding time for new members, of course.
This is true in theory but it’s famously hard to measure, having both very strong religious beliefs and notorious confounds. For example, is any particular anecdote telling you that a niche language like Lisp or Haskell actually makes people more productive or just that you’re filtering for developers who would be above average in any language?
Actually measuring these sorts of claims scientifically is expensive enough that it’s rarely attempted, but that rarely causes people to taper the degree of certainty people express. That doesn’t mean that new languages are bad but I think Fred Brooks was right to recommend skepticism of big claims rather than more incremental improvements.
Expecting any improvement to come with a flat learning curve is unrealistic at any job, and especially so at programming.
Monads like this are very problematic in the JavaScript world. I say this as an F# programmer, and our community is not unfamiliar with Railway Oriented Programming (https://fsharpforfunandprofit.com/rop/)
Unlike F# or any other decent functional language, JavaScript doesn't have very good constructs to guide and ensure the programmer is using these constructs properly, let alone using alongside with existing error propagating constructs.
What happens when you mix these with promises, or RxJs, or something else?
My advice would to be avoid using constructs like this for general purpose error handling and use tried and true constructs for Exceptions, which should be exceptional.
Something like this isn't entirely useless, sometimes its useful to encode errors in an abstract type when the errors become an integral part of the problem domain. Sometimes errors aren't exceptional and you need a way to deal with them that's somewhat performant. Even if that's the case, you need to be careful with constructs like this because it's easy to use them wrong, which might lead to scenarios where errors aren't properly thrown and propagated.
I'm not saying to never use them. I'm just heeding you should tread carefully.
OTOH you totally expect I/O errors and parsing errors while reading a CSV file. You totally expect a mismatch when matching a regexp. Using exceptions here is discouraged even by Martin Fowler, of all luminaries [1].
[1]: https://martinfowler.com/articles/replaceThrowWithNotificati...
>we use the term exception for expected but irregular situations at runtime and the term error for mistakes in the running program that can be resolved only by fixing the program. https://wiki.haskell.org/Error_vs._Exception
OTOH if you wrote a non-total function and it fails to proceed given some arguments, this is an error. You should not try to somehow continue if it happens; you should fix your program.
I don't see how this is different from when a file that's supposed to exist doesn't. I guess it's a matter of degrees.
That whole example in the linked article displays coding style that surely makes sense but I can't imagine having that as a theme in my codebase. It's fringe and not native to the language, which makes onboarding others to that codebase a pain.
But the benefit of them over a check against null is that you can write a pipeline of functions that dont have to handle those cases and you know that its always safe to do so. This other article by the same author as the OP I think illustrates it well https://jrsinclair.com/articles/2016/marvellously-mysterious...
Javascript has at least two mature type checkers you can use that will let you use these constructs just as safely as in F#.
What happens when you mix these with promises
In my either library I added rightMapThen and leftMapThen, and called it a day. worked absolutely fine. I think I also have rightFlatMapThen and leftFlatMapThen. Works flawlessly, and I have everything I enjoyed about the equivalent in F# (where I first encountered the concept).
That’s just incorrect. If it’s a pure function, it would always throw exactly the same exception given the same input. It would still be referentially transparent.
Sure, it’s a pure function with two exit paths, which is more of a hassle and possibly more error-prone to deal with. But it can still be pure, so you can still get the benefits of pure (dynamically-typed) functional programming.
Edit: to make sure we’re on the same page, I interpret “pure” as no side-effects and no non-determinism, so a call with the same arguments always has exactly the same result.
What side-effect does “catch” have? Something that’s visible externally, visible to the caller of the function doing the catch.
I suppose if you catch an exception and pull out the stack trace, that gives you some extra information about the caller. Even that doesn’t make you impure, though, if you treat the call stack as additional input.
That's the premise of the article and it is demonstrably false. You throw exceptions in a reproducible manner based on input, not a RNG!
function oneDividedByX(x: Double): Double
oneDividedByX(2) // 1/2
oneDividedByX(3) // 1/3
and so on.. Except with zero. With zero you get an exception, however nothing in function signature mentions about such result. You only expect a double
In Either case - it solves the problem, because the signature would be like this:
function oneDividedByX(x: Double): Either[ComputingError, Double]
As for throwing exceptions - it can be done in pure way, but catching exceptions cannot
The underlying error handling methodology however is awesome and I miss its guarantees and composability in my daily C++ work (yes I know, there's Boost Outcome, but they stripped it of everything useful before including it in boost, as far as I'm concerned).
It would be bad to name it after error handling, but the entirely meaningless names aren't great either.
As this association was extended to left-handed people, its use nominally perpetuates an ancient slur, but I think we can put that aside.
[1] https://english.stackexchange.com/questions/39092/how-did-si...
"Right" also has the meaning of "upright" (as in right angle) or, archaically, as "straight", and the normal processing path might be regarded as the straight one in the railway metaphor.
[1] https://www.reddit.com/r/etymology/comments/2cdshk/etymology...
https://www.etymonline.com/search?q=right
An interesting place this is preserved is in heraldry[0], and I've also heard it used in cards (a King, in red, facing the dexter).
[0] https://en.wikipedia.org/wiki/Dexter_and_sinister
Right is actually a synonym for Ok.. besides, Either is useful for _much_ more than error handling. That is exactly why I dislike Error for the left side; often it's just not an error.
If all went well, the result is Right.
If something went awry, you can still examine what's Left.
They have no "semantics" otherwise. You could call them "Jib" and "Jab," and it'd amount to the same thing. Simply because you ascribe a particular meaning doesn't change their nature. Hence why Left and Right.
Now I do recommend, for readability's sake, ascribing an alias to this type if it aids your understanding of the code when you use it in a particular way that gives your code a certain meaning!
They might convey the exact meaning well, but they (and I) would prefer a type with slightly different semantics that better remind us what each side actually represents.
Also, you might want to someday write:
And that wouldn’t make much sense if fast track is a so-called error condition.Having a type that is right biased doesn't mean it can't also implement the more generic interface. If your needs change you can always go back to using the less opinionated version with little breakage.
Rust's is notable in that they have both Either and Result. I think there's value in that, even if it is mainly for ergonomics and communication.
Also, aliases are fine, I could just see them being duplicated a lot. Probably not my first choice.
either isn't in std though, it's a third party crate. Personally, I think the core data structure should be as generic as possible. The reality is that if the Result type was named appropriately, the Either crate would be redundant.
What enum? We're talking about a specific data type, it doesn't make any sense to say 'an enum' is the generic version of Result.
I understand why they chose Result instead of Either. I just disagree with it; what you end up with is 2 data structures with the exact same structure and implementations but that represent different things. If you have Either in std, then Result is redundant, but not the other way around.
More fundamentally I disagree that Either makes Result redundant. Result has semantic meaning that's lost with Either.
No, it's not 'a generic', but it has generic type parameters. An enum can be any number of variants with any number of type parameters.
A result is: data Result a b = Err a | Ok b (or in Rust) enum Result<T, E> { Err(E), Ok(T) }
> It's essentially a shorthand instead of manually writing out the enum yourself.
I'm not sure which enum you're referring to. You do realise an enum can have any number of variants, not just 2? I could define a sum type with 16 variants. 'enum' is just Rusts keyword for defining sum types.
> Result has semantic meaning that's lost with Either.
Which is what?
Yes, that's what makes it more generic than Result. Indeed that's the only practical difference except for a few helper functions and a #[must_use] (which only makes sense for Result, not for a more general Either type).
>> Result has semantic meaning that's lost with Either.
>Which is what?
Seriously? Error checking! That's the whole point of it. One side has meaning over the other; it has weight. The "left" is the result you want. The "right" is when something gone wrong.
I think you have some fundamental misconceptions about sum types. Note that enums also don't even need to have any variants.
enum Never {}
Either way, this discussion is no longer fruitful. I encourage you to read about sum & product types.
But, since there's no abstraction that assumes that such a structure of sum-types is shaped like a proper list, there's nothing stopping you from building any arbitrary binary tree of types using Either. And so, like any other binary tree, the two branches get called Left and Right.
On the other hand, it may be easier to forget the error state if it were the second argument. As is often the case with Promises and JS developers.
At least there's a pun (right vs. left, right vs. wrong), which is probably why the pattern stuck, but I don't think that's actually sufficient justification.
A good way to remember which variant represents success in the case you're talking about an error is that Right is the "right" variant, Left is the "wrong" one.
A better naming for 99% of cases is (like found in Elm)
type Result error value = Ok value | Err error
I disagree - what if on success it returns a string representing some data, and on failure it returns a string detailing the error? A `string | string` return type is not very useful, while an `Either<string, string>` is IMO.
You could have:
type Result = {type: 'ok', value: string} | {type: 'err', value: string}
good thing firefox has a reader view. but really, this sort of design should be gone by now.