Elegant Error Handling with the JavaScript “Either” Monad (jrsinclair.com)

177 points by fagnerbrack 124 days ago

235 comments

cryptica 124 days ago

I don't find it elegant at all. I prefer to use native language features than inventing unnecessary abstractions.

>> 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.

    monocasa 124 days ago

    > I don't find it elegant at all. I prefer to use native language features than inventing unnecessary abstractions.

    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".

      cryptica 124 days ago

      It's important to separate coding mistakes from exceptions. For exceptions, I define my own classes derived from the Error object and I give each one a unique `name` property that way the error handler can decide how to handle the error based on its name. If the error happens on the server but is also meant for the client side (and is human-friendly), then I may add an `error.isClientError = true` property; then, in my catch handler, I know that these kinds of errors are safe to send to the client.

      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.

      mistersys 124 days ago

      > It's attitudes like this that leave you with a dialog box that says "an unknown error has occurred"

      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.

        monocasa 124 days ago

        For true errors like that, I agree with you, but the parent is basically saying both "fuck monads" and "only handle errors in your top level modules".

          kjeetgill 124 days ago

          I do think theres a 'tiering' that needs to happen for robust error handling, and I suspect you both agree more than disagree. Take the Go-ish handle everything at a lowest level vs. a Java-y handle everything at the top entry points (the parent's post exaggerated for effect) as two points for comparison.

          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.

          djakjxnanjak 123 days ago

          Putting a catch-all at the root doesn’t keep you from catching exceptions before they get there.

      lacampbell 124 days ago

      I prefer languages that allow you to build abstractions like this, rather than being beholden to the standards body.

      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.

        monkpit 124 days ago

        > I check it all with typescript, and it's perfectly typesafe

        /me waits for arguments to ensue

      edflsafoiewq 124 days ago

      I've found exceptions are really good for getting relevant context since you can always get at least a backtrace. This is one bad thing about errors-are-just-values; you get nothing besides what the callee decides to put in the error. Especially with monadic error handling, where it's so easy to just pass the error on up, you can easily get a error value from some deeply nested call and no idea what went wrong.

      acdha 124 days ago

      > 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.

      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.

        monocasa 124 days ago

        I'm not talking about ability to debug. I'm talking about the code handling errors gracefully.

        Also the world is a whole lot bigger than frontend dev.

          acdha 124 days ago

          This is an article about JavaScript where front-end work matters a lot directly and influences everything else. That widespread nature is also relevant to the question of whether you use the platform as intended or try to graft on semantics from very different environments. The latter approach has a track record of people spending more time maintaining ungainly hybrids than the imagined benefits saved.

    Sophistifunk 124 days ago

    I agree, I've never seen an article with "Monad" and "JavaScript" in the title propose anything even remotely approaching elegant, and very rarely do they even offer any functionality you don't get built-in once you're at async/await-level language support.

    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.

    SomeOldThrow 124 days ago

    Isn’t Promise a specialized Either? Seems like the async/await syntax could also easily be tweaked for monadic error handling. It allows you to either handle errors as values or as exceptions without forcing you to use blocks and explicit control flow.

    Edit: removed ill-thought-out remark.

jupp0r 124 days ago

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).

    agentultra 124 days ago

    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!

      switchbak 124 days ago

      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.

        erikpukinskis 124 days ago

        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.

          switchbak 124 days ago

          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.

            leshow 124 days ago

            > 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.

              ChrisSD 124 days ago

              Rust's "generic as possible" type is an enum. Result is a specialization meant for common error handling.

                leshow 124 days ago

                > Rust's "generic as possible" type is an enum

                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.

                  ChrisSD 124 days ago

                  What Result? Result is a generic not a specific type. It's essentially a shorthand instead of manually writing out the enum yourself.

                  More fundamentally I disagree that Either makes Result redundant. Result has semantic meaning that's lost with Either.

                    leshow 124 days ago

                    > 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.

                    Which is what?

                      ChrisSD 124 days ago

                      > 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.

                        leshow 123 days ago

                        > 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]

                        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.

      derefr 118 days ago

      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.

      jeremyjh 124 days ago

      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.

        123 days ago

    mannykannot 124 days ago

    The way I remember is that the Romans associated the left with bad things, with this ultimately making its way into English in the word "sinister" [1]

    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...

      ubertaco 124 days ago

      Yeah, I've heard the mnemonic "an Either contains either the Right answer or else whatever's Left", which is simple/punny enough to be memorable.

        dmix 124 days ago

        That perfectly captures what it does too. Nice one.

      koala_man 124 days ago

      And here I've just been using the fact that "right" also means "correct" in English

        mannykannot 124 days ago

        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.

        [1] https://www.reddit.com/r/etymology/comments/2cdshk/etymology...

      JoshTriplett 124 days ago

      I usually just treat it as a pun between "Right/Left" and "Right/Wrong".

      jupp0r 124 days ago

      Interesting, thanks for explaining.

    bjpbakker 124 days ago

    > 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.

    jessaustin 124 days ago

    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...

    nine_k 124 days ago

    The naming is totally mnemonic.

    If all went well, the result is Right.

    If something went awry, you can still examine what's Left.

    leshow 124 days ago

    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.

    dllthomas 124 days ago

    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.

    punjabisingh 124 days ago

    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.

    boubiyeah 124 days ago

    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)

    type Result error value = Ok value | Err error

      lacampbell 124 days ago

      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.

        boubiyeah 121 days ago

        But that's the simplest union type.

        You could have:

        type Result = {type: 'ok', value: string} | {type: 'err', value: string}

    taeric 124 days ago

    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.

      marcosdumay 124 days ago

      But there is a difference in meaning. Left shortcuts, Right doesn't.

      It would be bad to name it after error handling, but the entirely meaningless names aren't great either.

        jayshua 124 days ago

        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.

      hombre_fatal 124 days ago

      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.

        toastal 124 days ago

        But Either is also a Bifunctor so you CAN do to the Left what you do the the Right as well as unpacking the value from both sides.

      jupp0r 124 days ago

      I agree that Either is more general than error handling, but it's widely used for that in practice. The original article uses it for that, too.

    zergov 124 days ago

    The left one is always the error because the right is the right one.

zaroth 124 days ago

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.

Kind of like that sentence actually.

    steego 124 days ago

    You're very polite. I'm going to be more frank.

    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.

      nine_k 124 days ago

      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].

      [1]: https://martinfowler.com/articles/replaceThrowWithNotificati...

        mLuby 124 days ago

        Huh, I thought it was the opposite:

        >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

          nine_k 124 days ago

          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.

            firethief 124 days ago

            > 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.

        seppel 124 days ago

        Which are errors that you don't expect but you think you still can handle gracefully? I'm not a fan of exceptions but curious where you draw the line.

          netcraft 124 days ago

          not finding the requested record is a perfect case for an Either, or something that is potentially (legitimately) null. Thats not an exception generally.

            tastroder 124 days ago

            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.

              netcraft 123 days ago

              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...

      lacampbell 124 days ago

      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).

    munchbunny 124 days ago

    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.

      nine_k 124 days ago

      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.

      ehnto 124 days ago

      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.

        nine_k 124 days ago

        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.

          acdha 124 days ago

          > 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.

          ehnto 124 days ago

          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.

    ryanmarsh 124 days ago

    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.

      nine_k 124 days ago

      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.

        marmada 124 days ago

        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.

          nine_k 124 days ago

          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.

          [1]: https://pastebin.com/wY8ZC0mP

chrismorgan 124 days ago

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.

    lalaithion 124 days ago

    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:

        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.

      steveklabnik 124 days ago

      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.

      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

      ubertaco 124 days ago

      A use-case I've had for a quick right-biased Java Either I threw together is handling a migration from java.util.Date to java.time.LocalDate:

          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.

        james-mcelwain 124 days ago

        This could be accomplished using Optional#orElseGet without any additional library dependencies, though.

          ubertaco 123 days ago

          That's true. I didn't think of doing that. Nice!

      im3w1l 124 days ago

      This sounds clever at the expense of readability. Creating an explicit fallback mechanism for a Result would imo be preferable.

        pwm 124 days ago

        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.

      mpfundstein 124 days ago

      Interesting use case. I honestly only saw either up until now as a more advanced maybe

    agentultra 124 days ago

    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.

      jupp0r 124 days ago

      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.

        Gajurgensen 124 days ago

        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).

          seppel 124 days ago

          > 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.

            leshow 124 days ago

            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.

              seppel 124 days ago

              I'm referring to this comment: https://news.ycombinator.com/item?id=20224097

                leshow 124 days ago

                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.

        leshow 124 days ago

        Right is a synonym for 'correct'. How much mental energy does that take?

bpizzi 124 days ago

'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.

    alipang 124 days ago

    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.

      ljm 124 days ago

      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.

    e1g 124 days ago

    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.

    ishjoh 124 days ago

    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.

      taeric 124 days ago

      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.

      shawnz 124 days ago

      What advantage does this approach give over Java's checked exceptions, which have support built right into the language already?

        joshlemer 124 days ago

        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.

        darkkindness 124 days ago

        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)

    bcheung 124 days ago

    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.

      124 days ago

    agumonkey 124 days ago

    I wanted to coin a term for this. Failure oriented programming. State what it MUST NOT be, then the happy path will emerge.

      mpfundstein 124 days ago

      Very interesting. Can you tell more about that idea? Could we formalize that even ?

        agumonkey 124 days ago

        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 :)

    tsimionescu 124 days ago

    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).

diggan 124 days ago

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.

    signal11 124 days ago

    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.

      simcop2387 124 days ago

      It's likely that you just don't happen to have the font then, "Gabriele Light Ribbon FG" here's a place to see samples of it. https://www.wfonts.com/font/gabriele-light-ribbon-fg

        true_religion 124 days ago

        I have the font, and it looks readable to me.

        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.

    cheeze 124 days ago

    The whole site is incredibly hipster

      lilbobbytables 124 days ago

      I don't _think_ I'd call it hipster - hipster has a lot more white.

      This actually reminds me a LOT of making websites around 2006 - 2012.

    quickthrower2 124 days ago

    It reminds me of what svg text rendering looked like in 2001 using IE and the svg plugin from adobe.

    Retra 124 days ago

    >font that looks like it's been printed on paper with poor ink

    Actually, I think it is intended to look like mechanical typewriter output.

iainmerrick 124 days ago

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.

    zygimantasdev 124 days ago

    Your statement is true for throwing exceptions, however you cannot catch exceptions in pure way

      iainmerrick 124 days ago

      Why not?

      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.

Klathmon 124 days ago

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.

    asark 124 days ago

    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.

    the_duke 124 days ago

    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.

      Dirlewanger 124 days ago

      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.

      vishbar 124 days ago

      Scala's for-comprehensions (and Haskell's do-notation) help quite a lot as well.

      124 days ago

      lacampbell 124 days ago

      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.

    cheeze 124 days ago

    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.

    noonespecial 124 days ago

    Absolutely agree. This is very clever and it makes the "puzzle" part of my brain happy in the same way writing a program in "OOK!" does.

    That said, I work with quite a few juniors. You should absolutely not use this in production.

    ben509 124 days ago

    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.

      ufo 124 days ago

      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.

    bcheung 124 days ago

    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.

      toastal 124 days ago

      Are you required to use JavaScript? Because Reason and PureScript can both hook into React without much trouble and give you the features you want.

        bcheung 124 days ago

        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.

    roywiggins 124 days ago

    I have to admit, if I ran into this style in a real Javascript codebase, I'd run screaming into the woods.

    bcheung 124 days ago

    I've had good success using the Ramda library. It provides a lot of functional style programming helper functions but still feels good in native JS.

      Klathmon 124 days ago

      But that is almost exactly my issue with it.

      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.

      mpfundstein 124 days ago

      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.

        bcheung 124 days ago

        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.

leshow 124 days ago

I write haskell and love typed languages, but I would never use anything like this. The fact is that it's forcing a concept that can't be expressed in the language properly.

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.

    lacampbell 124 days ago

    I write haskell and love typed languages, but I would never use anything like this. The fact is that it's forcing a concept that can't be expressed in the language properly.

    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.

      leshow 124 days ago

      To start, there's nothing linking the left and right variants together in the implementation shown in the blog. In Haskell and F# (never written f# but I assume) left and right are variants of a data type. In this blog they are just 2 disjoint classes. Those classes just happen to have the same functions implemented, but there's nothing specifying that to be the case.

      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.

        lacampbell 124 days ago

        To start, there's nothing linking the left and right variants together in the implementation shown in the blog. In Haskell and F# (never written f# but I assume) left and right are variants of a data type. In this blog they are just 2 disjoint classes. Those classes just happen to have the same functions implemented, but there's nothing specifying that to be the case.

        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.

          leshow 123 days ago

          > In my javascript (well, typescript) version,

          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.

            lacampbell 123 days ago

            I'm critiquing the implementation shown in the blog, not your hypothetical implementation in typescript that I've never seen.

            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.

      sideshowb 124 days ago

      The lack of syntax alone puts me off. E.g. Inability to define an operator for composition

        lacampbell 123 days ago

        Yes that's annoying. It looks like JS will be getting the pipe operator soon. But Ocaml didn't have to wait around for it, since they can define operators in code.

tjansen 124 days ago

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.

    lmm 124 days ago

    > 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.

    bcheung 124 days ago

    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.

Vanit 124 days ago

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.

    samhh 124 days ago

    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`.

      heavenlyblue 124 days ago

      What if you miss one of these exceptions?

        Spivak 124 days ago

        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.

          mort96 124 days ago

          So essentially Java checked exceptions but which also applies to descendants of RuntimeException?

            Spivak 124 days ago

            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.

              heavenlyblue 122 days ago

              It’s all nice and easy, but how would you handle OOMs in that scenario?

              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.

vga805 124 days ago

I would like to read the article but between the background and the code font, I just can't do it. What is the thought behind this design decision?

Stevvo 124 days ago

"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!

    zygimantasdev 124 days ago

    Imagine a signature like this:

    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

    ben509 124 days ago

    Yup. Pretty sure the most referentially transparent function will stop if you unplug the power.

shawnz 124 days ago

> 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"

    roywiggins 124 days ago

    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.

ww520 124 days ago

Promise as a poorman's monad to do railroad-style programming works surprisingly well, even for non-async functions. I often do,

    Promise.resolve()
        .then(dothis)
        .then(dothat)
        .then(doit)
        .then(domore)
        .catch(e => log.error(e))
It mixes well with regular try/throw.

    WaxProlix 124 days ago

    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)

      ww520 124 days ago

      Not many people get the hang of Promise style programming. People prefer async/await.

jtdev 124 days ago

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.

    namelosw 124 days ago

    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.

      dyeje 124 days ago

      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.

      124 days ago

    lmm 124 days ago

    > 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.

      jtdev 124 days ago

      All language keywords are “magic” to some degree. The control flow that try/catch creates has never been surprising or invisible in my experience.

        cheeze 124 days ago

        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.

          lmm 124 days ago

          > 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.

            cheeze 124 days ago

            > 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)

              agentultra 124 days ago

              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.

              IanCal 124 days ago

              I think Maybe, Either, etc. are just implemented in regular Haskell, they're not special things in the language.

            true_religion 124 days ago

            Almost all special purpose control syntax can be replaced by a series of function calls, but the value of syntax is clarity of intent.

          RHSeeger 124 days ago

          > I'd be willing to bet that not even 10% of programmers could actually explain what a monad is.

          To be fair, I don't think you need to really understand what monad is to understand Either.

          Fellshard 124 days ago

          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.

        lmm 124 days ago

        > 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.

          Udik 124 days ago

          > Every keyword adds complexity to the language

          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.

            lmm 124 days ago

            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.

          venuur 124 days ago

          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.

        dropofwill 124 days ago

        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.

      Merad 124 days ago

      I'm struggling to see how try/catch is either magic or surprising unless you simply aren't familiar with exception handling.

      wvenable 124 days ago

      > 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.

        lmm 123 days ago

        > If you added multiple returns to every function and manually propagated every error you'd get exactly the same thing as try/catch.

        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.

      124 days ago

    MapleWalnut 124 days ago

    Maybe in plain JS, but with Typescript this Either pattern will force you to handle errors unlike untyped try, catch.

    cousin_it 124 days ago

    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.

      roblabla 124 days ago

      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.

        kmonsen 124 days ago

        So in rust you write with result and in js you use try/catch. Pretty simple, and readers will understand the code.

          roblabla 124 days ago

          Oh I agree, and definitely don't recommend using Result in JS, the language is just not tailored for it. I was specifically addressing

          > I never understood the love for Either-based error handling in Haskell and other languages.

      lmm 124 days ago

      > 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.

        cousin_it 124 days ago

        > 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.

        By the empty throws clause.

          lmm 124 days ago

          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.

            cousin_it 124 days ago

            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".

              lmm 124 days ago

              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.

                124 days ago

                cousin_it 124 days ago

                I think you can often avoid reasoning about all possible paths, by having one try-finally block per each resource to be freed.

            124 days ago

      0815test 124 days ago

      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.

        cousin_it 124 days ago

        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.

          esarbe 124 days ago

          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)

            124 days ago

        124 days ago

      chii 124 days ago

      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?

    ahallock 124 days ago

    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.

nielsbot 124 days ago

I feel like TypeScript would make this easy?

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 }

        ...
    }

    remram 124 days ago

    I also feel like this approach is a lot more valuable in languages that use static typing, such as Rust, Java, and TypeScript. In a yolo-typed language, it is just as easy to forget to handle the return type or the thrown exception.

ncmncm 123 days ago

"Sum types" fill the opposite role in strongly- to that in in dynamic-typed languages.

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.

vinylkey 124 days ago

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.

giaour 124 days ago

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.

dakom 124 days ago

Several comments here are along the lines of "I haven't done much monadic error handling but here's why it's bad."

That's silly because the benefits of this type of programming can't be grokked until you use it on real projects.. it's honest-to-goodness not enough to look at small examples and judge whether it's worthwhile or not.

There's a lot of this realization going around actually - for example Rust's main raison d'être is basically: "look - we _can_ write safe c/c++, some of us can even nail it perfect every time in small examples, but in large apps it's inevitable that we'll all make mistakes"

Here's two issues with try/catch in real-world apps:

1. You aren't forced to write it. It's simply possible to forget that a function might fail (e.g. when that failure is defined as a side-effect). This is even worse when there's no type system forcing you to think about it. We all make this mistake!

2. You can't easily distinguish between expected errors and unexpected errors - and even where you can, dealing with that becomes a big mess.

Unexpected errors (like "out of memory", "computer is quitting", "electricity is gone") - _should_ crash and burn and stop the app (unless you explicitly have a way to recover from it - in which case it's actually an _expected_ error). Letting the app continue on where there's undefined behavior is dangerous.

Expected errors (like "user not found", "wrong password", perhaps even "network down") need to be dealt with. Every single time. Even in a function where it might not happen 99.999% of the time - that one rare time where it happens is especially hard to duplicate and fix.

That said, I agree with the comments that say it's a bit awkward in JS, and much less effective without a type system forcing the right constraints. I like using FP-TS[0] with typescript for extra help at "transpile" time, but Sanctuary[1] (based on FantasyLand[2]) with the runtime checking turned on is pretty incredible too.

With _some_ type system making sure that we're following the rules, monadic error handling is definitely more elegant and safer than try/catch.

[0] https://gcanti.github.io/fp-ts/modules/ [1] https://sanctuary.js.org/ [2] https://github.com/fantasyland/fantasy-land

    Klathmon 124 days ago

    I pretty much agree with you.

    I hate error handling in JS. It's messy, it's really REALLY hard to get right, every single project has to layer on their own ways of handling it since the only thing the language gives us is the ability to `throw` and `catch` (even the "standard error" object doesn't have anything other than a place to put a string).

    It's a giant mess, and I'd love to find a way out, but I think it has to come from the language itself. Trying to shoehorn JS into a fully functional language leads to a lot of scaffolding and "custom" piping to get it to work, and in 100% of the situations I've seen it done it's been harder to maintain the scaffolding (and wrap 3rd party code to work in the way you need it to) than it ever would be to manage exceptions.

    I'll admit to be fairly ignorant of the different ways of handling exceptions in other languages. For the most part I've really only experienced try/catch, multiple-returns (golang style), and whatever bastardization JS is at now (try/catch + .then/.catch + custom error objects) in production codebases. But that solution needs to be part of the language not bolted on in userland. Mainly so that you don't need to write all this piping to get the rest of the ecosystem to behave in the way you want/need.

    FWIW Rust's Result type is beautiful and seems to solve a lot of issues (from the very small amount of experience I have with it), but trying to use that in JS is only going to lead to trouble without some help from the language itself.

    amalcon 124 days ago

    > 1. You aren't forced to write it. It's simply possible to forget that a function might fail (e.g. when that failure is defined as a side-effect).

    When the compiler does require you to explicitly acknowledge exceptions (Java), people hate it.

      chowells 124 days ago

      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.

      iainmerrick 124 days ago

      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.

        Retra 124 days ago
          iainmerrick 124 days ago

          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.

    jerf 124 days ago

    "That's silly because the benefits of this type of programming can't be grokked until you use it on real projects.. it's honest-to-goodness not enough to look at small examples and judge whether it's worthwhile or not."

    But you can look at small examples and decide that the syntax noise isn't worth it.

    It's almost always a good idea to write "$LANGUAGE in $LANGUAGE" and not write "Fortran in $LANGUAGE" or "Haskell in $LANGUAGE". Importing these idioms into JS is pretty much the latter. You pay costs you don't pay in Haskell, and you don't get advantages that you can get in Haskell. It shouldn't be a surprise that the cost/benefit analysis for the constructs in JS can result in a very different one than the one for Haskell. You can't just substitute the analysis for Haskell and assume that it's all the same everywhere else.

    It's not "elegant". It's importing what is an elegant idiom in another language into a language where it's klunky, poorly-supported, and contrary to the grain of the language. It isn't elegant in JS just because it's elegant in some other language. It's exactly what it looks like: a klunky, choppy way to program in JS that requires the creation of a ton of functions that only a few outliers would normally think deserves its own function, making following this code require a lot of jumping around in the code base.

    And that's assuming it is what it says it is, which, as near as I can tell, it isn't. If I'm reading this right, it doesn't seem to properly short-circuit on errors properly, rather doing a rather odd thing where it tries to pass the value along down the line, which is at the very least going to have performance consequences and almost certainly major correctness consequences as this scales up. It's actually not even an implementation of "monad", but a sort of chopped up version to try to avoid creating even more functions, which is what is really necessary to correctly implement the "monad" interface. In Haskell do notation, every single "<-" is actually an entirely new function, nested inside the previous one, so:

        do
            thing <- whatever1
            morething <- failableLogic arg thing
            things <- mapM stuff [thing:arg2]
            return head things
    
    If "failableLogic arg thing" fails, that function ends up returning, and the next two lines "don't execute", that is, the nested functions don't ever get invoked. The "return head things" is actually a function nested in a function nested in a function nested in a function. (I may be off by one.) I think the core reason there's a lot of blog posts about how to "implement monadic stuff" in all kinds of other languages and no code in the wild that actually uses any of it is the requirement for all the nested functions. (You usually can't "unnest" them either due to the fact you're almost always closing over values from the previous functions.) It is astonishingly inconvenient in languages that don't have the requisite features and support, to the point you can fairly reliably guess that if someone has an implementation that isn't staggeringly inconvenient to use, they've missed something fundamental.

    Even the languages that sorta kinda support this, don't, really. They encode specific bits of functionality vaguely inspired by monadic handling, but out of the Haskell family of languages I don't think I've yet seen a full, true implementation of "monad" in general that anyone uses.

    (When writing "$LANGUAGE in $LANGUAGE" isn't adequate for a task, it's almost always the right answer to go get a new $LANGUAGE.)

      0815test 124 days ago

      > 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.

      dakom 124 days ago

      > 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.

      esarbe 124 days ago

      Scala's for-comprehension work quite good. It is part of idiomatic Scala and works for all monads.

        for {
          thing <- whatever1
          thing2 <- mightFail
        } yield (thing, thing2)

turdnagel 124 days ago

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.

    bcheung 124 days ago

    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.

      vikingcaffiene 124 days ago

      Ah yes long division of Roman numerals. Can’t count the number of times that’s come up...

      turdnagel 124 days ago

      This appears to me to be a matter of personal preference rather than fact; could you explain what you mean by "error prone and tedious"?

        bcheung 124 days ago

        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.

dustingetz 124 days ago

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.

z3t4 124 days ago

It's a bless to be ignorant about all side effect and all the way something can fail. It makes you very productive, and produce clean code, clean as in not littered with resilience management.

inlined 124 days ago

Where functional case classes are available natively I love them. I wonder though why the author didn’t use the Scala triple of Try[T] and its children Success[T] and Error

rienbdj 124 days ago

Without a type system complex monad stuff is very easy to mess up.

124 days ago

galkk 124 days ago

Dropped reading article after one screen. Too hard to concentrate, especially for the code blocks font.

bdorn 124 days ago

the design of this site is unreadable. skeuomorphism is a bad design pattern (blotchy paper, really?) & is distracting from the content.

good thing firefox has a reader view. but really, this sort of design should be gone by now.

124 days ago

thanks_ihateit 124 days ago

Ha! I love the fact that both the hideous JavaScript/functional drivel and the garish, obnoxious font and page layout work together to produce a revulsion that has something everyone can hate.

It's like the web v2.5 analog of an Excel spreadsheet typeset entirely in Comic Sans.

A near total antithesis of "elegant" error handling.

lngnmn1 124 days ago

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).