Elegant Error Handling with the JavaScript “Either” Monad
jrsinclair.com
jrsinclair.com
>> and we all intend to put that try…catch block in place. Really, we do. But it’s not always obvious where it should go. And it’s all too easy to forget one. And before you know it, your application crashes.
Actually this is a good thing, it encourages you to make your code into hierarchical, tree-like structures. Then you have a try-catch at the root of each tree. Some apps have only one root (e.g. the program's entry point), others have a few (e.g. API endpoints on the server); it depends on how many kinds of parallel/async workloads your system has to handle but generally this should be obvious; if your system's entry points are not obvious to the point that you can 'forget' to wrap try-catch blocks around them, it's typically the sign of a bigger architectural design flaw.
>> Another thing to think about is that exceptions make our code impure. Why functional purity is a good thing is a whole other discussion.
Yes, it's a whole other discussion in which many developers might take an opposing stance. Personally, I think that co-locating state and logic is key to achieving modularity and clear separation of concerns; OOP is likely the best way to achieve that. I do think that functional purity is good in some cases, but not the majority of cases.
A lot of the large functional systems that I've seen were highly deterministic, highly robust spaghetti code; they are difficult to make sense of and can become impossible to maintain.
I prefer languages that allow you to build abstractions like this, rather than being beholden to the standards body.
> Actually this is a good thing, it encourages you to make your code into hierarchical, tree-like structures. Then you have a try-catch at the root of each tree.
That leads to a complete lack of error handling, because you've lost all of the relevant context of what you were trying to do. It's attitudes like this that leave you with a dialog box that says "an unknown error has occurred".
20 years ago, maybe. These days every browser has a debugger which will automatically take you to the exact location with full context at every layer of the stack and tools like Sentry will collect and aggregate those reports even from devices you don’t control.
Also the world is a whole lot bigger than frontend dev.
For many errors, "an unknown error has occurred" is the right message, since it's a bug. There's no action the user can take to fix it, and you should let the user know to try again later or contact support. The message can then be sent to your error reporting software so you can fix the bug.
Sure, you can explicitly call "An unknown error has occurred" dialogue on every error case, but instead, I think the ideal tradeoff is to handle all errors that you predict will happen regularly, (ex. 404, authentication errors, etc.) on the value/monad level and handle errors you don't expect happen via the throw level.
Disclaimer: This is a variant of my Java Exceptions vs. Go err conversations. I'm working Either into it.
The Go-like approach can often force you to handle errors at the lowest level. You often have details of the error but you're without context of the program. An example is a zlib library; you don't want to handle the failure of any particular `file.read(buff)` call but rather just fail at a higher level `unzip_file(src, dest)`, and with context make choices in the program from there.
Now you can do this in Go as well, you just manually unwind returning err to the level above. It's verbose compared to exceptions (but more explicit...ish). I suspect most programs will end up resolving things at about the same level.
That's the thing with Either(), since it's doing the catching you clearly have exceptions to work with too. In terms of error handling the interesting bit is what 'tier' you handle things at, the Either vs Try/Catch are like 80% the same.
Of course, it takes some discipline to come up with a consistent and well defined error-handling strategy, but IMO it is not difficult. Also it's not a problem if a third-party library doesn't agree with your error-handling approach, it's often a good idea to re-create and re-throw errors from third-party libraries to provide more context anyway. That said a lot of JS libraries do use a similar approach as me for error handling, but sometimes they might use an error.code property instead of error.name but it doesn't really matter.
The worst is if a third party library throws plain strings, but that's also often not a huge problem either because my own logic can make sense of the context and re-throw a proper SomeThirdPartyLibraryError which incorporates the original error string; it's just a little bit of extra work.
I use Either types in javascript. I coded them myself (I like mine better than those in the article, but I would wouldn't I?). I check it all with typescript, and it's perfectly typesafe, same as it would be in haskell or ocaml.
So Javascript is 100% a language that allows you to build abstractions like that.
/me waits for arguments to ensue
Edit: removed ill-thought-out remark.
Yes, monadification makes it easier to construct new functionality out of tiny parts, I'm not arguing with that at all. What it costs though is more than performance and debugability, but just simple readability goes right out the window, as a rule.
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...
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).
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.
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!
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.
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.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}
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.
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).
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:
makeRegex :: String -> Either Regex ParseError
makeRegex regexExpression = makeBasicRegex regexExpression >> makeLookaheadRegex regexExpression
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
List<LocalDate> dobs = users.stream()
.map(person -> Either.either(person::getDeprecatedDob, person::getDobLocalDate))
.map(oldOrNewFormat -> oldOrNewFormat.fold(
(Date oldFormat) -> convertOldFormatToNew(oldFormat),
(LocalDate newFormat) -> newFormat,
))
.collect(Collectors.toList());
I use the right-biased-ness of the Either to prefer the new format, if available, but failing that, use the old format and then convert as needed.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).
This 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.
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.
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.
And in the end you haven't gained anything, the functions that you use inside the block can still throw exceptions so you need a try/catch anyway.
It's cool as an exercise, but I would never use it practically (in js). In other languages that fully support sum types and errors-as-values (like Rust). I find it a joy to work with and like it much more than exceptions.
As someone who first discovered these kinds of types in F# and then ported them over to Javascript/Typescript - please explain why it can't be expressed in the language properly. Other than syntax it feels no different to me. It's been a great benefit to my programming.
Objects have always been able to do a lot of what ADTs do and vice versa.
There's no support for sum types in js, or good support for pattern matching. If you like to use your home-grown Either type in js then more power to you. But there's no denying that you're shoehorning a concept into the language that it doesn't have good support for.
In my javascript (well, typescript) version, I use an abstract base class and override the rightMap, leftMap etc versions. These concrete classes are internal, only the abstract one is exported.
There's no support for sum types in js
Using TSc is a linter, there 100% is support. I'd be happy to demonstrate if you're interested. Though it's a bit beside the point, despite having access to them I didn't implement them that way as I prefer method chaining.
or good support for pattern matching.
What pattern matching do you need for an either type? I enjoy pattern matching for lists, but even when using languages with pattern matching I don't use them for Option/Maybe, or Either. I was under the impression it was an anti-pattern to use pattern matching where a suitable function existed.
But there's no denying that you're shoehorning a concept into the language that it doesn't have good support for.
I deny it. IMO you can create a perfectly usable Either Type in any language that supports classes, objects and generics (well, maybe not C++). There's a lot I miss in JS from Ocaml and F#, but monadic error types are not one of them, because they're easy to implement and just as ergonomic.
There's more than two ways to skin a cat, and after going back and forward between OO and FP for a while I realised I could accomplish the bulk of what I wanted in both paradigms.
I'm critiquing the implementation shown in the blog, not your hypothetical implementation in typescript that I've never seen.
> What pattern matching do you need for an either type?
In nominal type systems like Rust/Haskell/F# you want to be able to match on the constructor. In typescript where '|' means something slightly different you need to fake a type constructor by adding a type: "variantname" property
> I deny it. IMO you can create a perfectly usable Either Type in any language that supports classes, objects and generics (well, maybe not C++).
Javascript doesn't support those things.
That was not what you originally wrote:
The fact is that it's forcing a concept that can't be expressed in the language properly.
Your claim was that it cannot be expressed in the language properly, and I am disagreeing.
In nominal type systems like Rust/Haskell/F# you want to be able to match on the constructor. In typescript where '|' means something slightly different you need to fake a type constructor by adding a type: "variantname" property
You didn't answer the question, what useful pattern matching can you do with Either/Option? Give me pseudocode even.
Javascript doesn't support those things.
A few jsdoc comments and tsc command, and hey presto - it does.
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.
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.
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
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"
Promise.resolve()
.then(dothis)
.then(dothat)
.then(doit)
.then(domore)
.catch(e => log.error(e))
It mixes well with regular try/throw.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.
You can define your functions as returning Error or some other type:
function something(...):Error|<something else> {
...
}
Also, define `isError(...)`, like so: function isError(value: Error | any): pet is Error {
return Object.hasPrototype( value, Error )
}
You'd still have the if clauses, but maybe that's not so bad: function doStuff():Error|Result {
let value = functionReturningValueOrError()
if (value is Error) { return value }
let value = otherFunctionReturningValueOrError()
if (value is Error) { return value }
...
}In Haskell, Rust, and C++, a "sum type" weakens type enforcement to provide a data-flow path paralleling another for, we hope, a short distance, before the paths peel apart at a convenient spot -- typically some sort of pattern match construct.
In a dynamic-typed language like Lisp, Javascript, or Python, they appear in more or less the same place, but their role is, instead, to impose some discipline, to make visible what could otherwise be an implicit substitution of one type for another.
In other languages, I normally use option types when things can fail in unexpected ways, which almost always means I/O.
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.
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:
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.)
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.
for {
thing <- whatever1
thing2 <- mightFail
} yield (thing, thing2)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.
good thing firefox has a reader view. but really, this sort of design should be gone by now.