1. 27
    1. 32

      I prefer values because:

      • They are explicit (even though some languages make exceptions explicit)
      • They don’t add annoying special syntax
      • In some languages they are faster (because they don’t need to have stack traces, etc., and there is a differentiation between wanting one and not)
      • They don’t have to be “exceptions”. Of course if you consider something an exceptional event then you want a stack traces and as much debugging information as possible, but often enough they are more like booleans. Take a file cache for example. A file not existing should not be an exception and even if you wrap it in a “DoesExist?” function you will generate a stack trace, etc. in many languages
      • It makes programmers not think about them. Often exceptions are like nulls - not handled at all. Programmers are way more used to handle values, unless the language actively forces you to. And if it does there will be a lot of “basically not handled handlers”
      • There often is also the question “is the try a block?” “is the catch a block?” “What should go into each of them?” When you have a value you can combine it more easily. So often I see that either done wrong or the code being verbose and hard to read because it works around this, especially in JS.

      Some of the above is more philosophical (for the lack of a better term), but since programming languages are complex, abstract and artificial interfaces to interact with computers I think it is important not to only go into theoretical perfect situation usages, but also into how the users (developers in this case) will use them. And the sad state is that exceptions way too often seen as the “just do a big try catch around stuff”. While one cannot really blame the language for it this is how real life usage is. And purely from my own experience just having values currently works better.

      I think there is a bigger overall problem and while it feels like the article covers it implicitly is that it depends quite a bit on language and its implementation. Especially for exceptions there are better and worse designs. Sometimes they are just a horribly bad hack to return that “additional value, oh and let’s snort out a hard to parse and log stack trace as well”. They feel like they were just tucked on to the language to convey that an additional state. Sometimes simply copied from a language that does better. The fact that errors and exception are also not fun in most situation probably doesn’t help. People just wanna forget about them.

      The whole values vs exception discussion is really great. I hope this brings up more thought on them and maybe some new and approaches. Rather than just doing whatever your language’s version of try { throw Error } catch (error) { print error } or worse having that put in by your IDE/AI/…

      1. 9

        And the sad state is that exceptions way too often seen as the “just do a big try catch around stuff”

        In my experience, the alternative is not “properly handling every error case”, but “inproperly do something that looks like that, or straight up swallow errors”, which is objectively worse than a proper global exception handler logging the error case with a stacktrace and concrete lines of where it happened.

        People will spend the same amount of energy on error handling regardless of mechanism, if they do want to make it decent, they can in both cases. If they currently don’t want to think about the unhappy path though (which at times may be a completely reasonable, correct choice), they will just unwrap/ignore stuff.

        1. 1

          People will spend the same amount of energy on error handling regardless of mechanism

          I don’t think that is true in real life, even though one would think so. Maybe it’s that errors just being ordinary variables/return values makes some developers more confident.

          If they currently don’t want to think about the unhappy path though (which at times may be a completely reasonable, correct choice), they will just unwrap/ignore stuff.

          It’s a valid choice, but it should be a conscious one. Again, not sure why that is but even in JavaScript it seemed that with node.js initial “errors are passed into callbacks” people were more conscious about them. I am not sure, but it seems to be more a psychological thing. Maybe it would be enough to in some languages slightly change the syntax.

          Maybe it’s also calling it errors vs exceptions. Really not sure here, but I am sure we live in a world where there are too many blind try { ... } catch (e) { log(e) } or worse scenarios. And while I know it is a discipline thing and I wouldn’t disagree, but given that there appear to be way more issues in languages where it looks like in the mentioned pattern I think it’s something worth looking it.

          And given that many newer languages do error handling at least a bit different there is room for improvement.

        2. 7

          I think there’s an interesting case for having both, e.g. Elixir (sort of). APIs where it’s expected that an error is recoverable return a result struct, while assertions cause the process to crash and the supervisor figures out how to handle it (maybe not exactly the same as exceptions, but similar). I think this model is especially nice for a high level language that you would use for a web backend. If the language APIs return result objects by convention, you explicitly have to handle the error. But you also have the option to unwrap an error value into an exception and let the web framework catch it. That way you can avoid threading a result object all throughout your functions.

          Also I don’t know if you’ve seen this post, but it’s an excellent overview of values vs exceptions: http://joeduffyblog.com/2016/02/07/the-error-model/

          1. 3

            An expressive enough language can easily represent error conditions as values, no need for language-support. So for this reason alone, I think exceptions are superior - where they are needed (in exceptional situations, e.g. a filesystem error) they are better and can’t really be mimicked. While expected error cases, like parsing an integer should - imo - use a Result-type.

            1. 2

              It needs language support by convention rather than by language level support. You could use a result object in Java, but the entire stdlib and most libraries use exceptions so you’d need to manually wrap a lot of calls.

              I think the context of the language and application also matters. If I’m in Elixir doing a web app, yeah I’d probably just want an exception. If I’m writing a database in C or Rust, I’d like to handle the file system errors early. That’s why IMO having APIs return values by default with an easy way to turn them into exceptions seems ideal.

              1. 3

                It’s definitely part of the culture of a language, but you can nonetheless use it for your functions where it makes sense - Java now has proper algebraic datatypes.

                But as others have already noted - even Haskell makes use of exceptions as well.

                1. 2

                  I use result objects a lot in C#, even though they’re not conventional in the language. I use them more to avoid returning nulls from my own functions (e.g., when an ORM returns null for a not-found query) than for avoiding throwing exceptions as such. The failure value in the result object may be an exception object in case I want to pass extra information to the error handling code. If the error handling code needs to throw it, it can, but I don’t think I’ve ever actually done that. Like ~gf0 suggests, I like result objects for errors I expect to happen sometimes, and exceptions for totally unexpected things where log-then-die is the fallback.

              2. 1

                And Golang and Rust, though their panics are not as unconditionally fatal as Midori’s abandonment.

                1. 1

                  Yeah I feel like they’re in kind of a grey area because people say you’re not really supposed to catch panics most of the time, but people do it for web frameworks anyway. Elixir and Erlang basically encourage you to throw and give you different handling strategies.

                  1. 7

                    The nice thing in BEAM is that actors (processes) can die and have a monitor that’s notified when they die. This tends to be what I use exceptions to model: something happened in this big pile of code that caused it to go wrong, handle failure of an entire component.

                    Doing this well is hard in a language that has shared mutable state.

              3. 6

                I agree with all your points. I would never go back to implicit errors. The try/catch construct is a terrible way to deal with errors.

                1. 2

                  While some modern languages have abandoned exceptions for tuple returns and results, others have surely improved them, right?

                  I can say that at least Swift addresses several of your points with its throw—It’s explicit, you can make any type conform to Error and be throwable, you’re forced to handle the throwing case just like you’re forced to handle an optional’s null case, and you can read which lines of a function involve potential throwing versus which are straight line code for sure.

                  1. 2

                    Swift doesn’t have an exceptions system, it has an exceptions syntax around error out parameters (basically what obj-c did). The syntax is there because using it raw would be worse than what Go does, which is saying something.

                    It has since added results, though I don’t know how much they’re used.

                    1. 1

                      I think you may be thinking of the translation of ObjC NSError ** method signatures for interop, which are effectively a syntax upgrade and an implicit conditinal throw.

                      However, when Swift calls a throwing method with try, including these translated interfaces, the throw will from the programmer’s perspective jump out to the nearest surrounding catch, as far up the call tree as necessary, where they can then handle it or rethrow. I’m not aware of the internal details, but on a language user level, it’s pretty much familiar try-catch-finally exceptions. This has been the case since throws was added in I think Swift 2. Typed throws is new this year.

                      When you say “exception system,” do you mean something more specific than that?

                      We don’t use Result so much anymore, but we did come to love it when sum type enums were novel. We used to roll our own Result type to use in completion handler functions and promise results. Then they added it to the stdlib, and then async/await removed the need for most of those callbacks and let us use return and throw for async completion, the same as we always did for synchronous code.

                      1. 3

                        I think you may be thinking of the translation of ObjC NSError ** method signatures for interop, which are effectively a syntax upgrade and an implicit conditinal throw.

                        No.

                        when Swift calls a throwing method with try

                        See that’s the thing, you have to use try on every faillible call, and that performs the relevant transformation.

                        it’s pretty much familiar try-catch-finally exceptions

                        Except for the part where you must try every call for this to work, which is the glue which handles the error values.

                        That behaviour is exactly what you get out of Rust’s results and ?, and that’s not an exception system by anyone’s reckoning.

                        1. 1

                          You left out this bit when quoting me:

                          on a language user level, it’s pretty much familiar try-catch-finally exceptions

                          Swift’s do-catch-finally + try + throw provide familiar try-catch-finally + throw syntax and behavior to the person writing code. If you insist there’s no exception system in Swift, would you please draw a distinction to show it?

                          To my limited knowledge Rust’s Result does not do that, it does the other thing under discussion—it does what Swift’s Result does, which is carry a success or error value in a sum type you can return or pass as an argument, one stack frame at a time, which you can then consume with pattern matching or some other syntactic convenience. Are Rust’s results and ? somehow closer to Swift’s error throwing than that?

                          See that’s the thing, you have to use try on every faillible call, and that performs the relevant transformation … the part where you must try every call for this to work, which is the glue which handles the error values.

                          When there are nested throwing calls in an expression, you need only write try once before the entire expression, not once per call. What’s necessary to do it at all is that you be in a throw-capable context. This is also true of await; you have to write it once before an expression of nested async calls, but you must be in some async context to do it.

                          I’ve always understood that Swift’s try (and await) is a noop, that is required so that the reader will know the lines on which program flow may escape (or suspend), and that the propagation and handling are provided by a surrounding throwing context—just the same as await in async contexts.

                          So yours is a surprising if plausible interpretation. Do you have a source for it? I’m prepared to be wrong because I’m finding it hard to search for supporting evidence with this set of words, and though I’ve used the language since beta, I’ve never been a language contributor.

                          1. 3

                            Are Rust’s results and ? somehow closer to Swift’s error throwing than that?

                            ? immediately exits from the current scope (function at the moment though there are plans to allow it at the block level eventually) on failure, which broadly matches the behaviour of Swift’s try. Because it also includes a conversion component, as long as the Err side of the result implements Error you can always type-erase it to Box<dyn Error>.

                            The caller-side handling in that case is a bit more complicated as Rust doesn’t have syntactic sugar for downcasting but technically nothing precludes a declarative macro which would take types, patterns, and predicates and go through the cases providing an interface similar to swift’s catch.

                            It’s not the commonly done thing as precise errors are more common (if only to avoid boxing) but it’s not exactly beyond imagining.

                            So yours is a surprising if plausible interpretation. Do you have a source for it? I’m prepared to be wrong because I’m finding it hard to search for supporting evidence with this set of words, and though I’ve used the language since beta, I’ve never been a language contributor.

                            You can see that errors are transmitted through an output register in the calling conventions summary, rather than use unwinding / stack walking.

                            1. 1

                              Thanks for all the detail—I see what you mean by “exceptions syntax around error out parameters”. The Swift book’s error handling introduction also draws the distinction that it does not do stack unwinding. In this article’s context, that places it pretty well in the result object camp.

                              I’m getting the picture that the difference between Rust’s Result and ? and Swift’s error throwing is that Rust places the details of propagation in optional syntax, the ? macro, while in Swift it’s provided by a throwing context, and that context and try are simply required to call the function at all. It’s a tradeoff of a function color for stronger guidance.

                              I wonder if either or both languages would optimize away the extra cost in the case that the error type parameter to Result<> or throws() is Never?

                    2. 1

                      Yes, exactly! That’s what I tried to say with it’s good that there is movement and people think about exceptions.

                  2. 21

                    I don’t find this article a good defense of exceptions. It has a strong bias towards a specific error handling scheme: “retrieve error at the top level, log it and continue”. This basic scheme hides a lot of the drawbacks of exceptions. Furthermore, it keeps conflating the properties of exceptions and results with things that are independent of these two, but relevant to the languages being considered.

                    Exceptions Are Much Easier to Work With

                    For the initial author of the code, they might. The hidden early returns caused by exceptions will make maintenance harder, though.

                    Exceptions make this extraordinarily easy. No matter where you are, 100 call stacks deep in your little corner of the program, you detect an error, so you throw an exception. All the hard work is now done automatically in stack unwinding

                    Having functions that take result on the entire stack is what you want and will give you the same benefit. This is more about having RAII than exception vs results.

                    Boilerplate

                    ? is no boilerplate. It is a single character, and it is load-bearing. It means that the operations is fallible.

                    Do you add an error result whenever a function asserts something?

                    No, Rust has panics for this, which has the same as exceptions.

                    Allocations can fail, the stack can overflow, and arithmetic operations can overflow. Without throwing exceptions, these failure modes are often simply hidden.

                    Conflating the decision of aborting on memory allocation failure (motivated by Linux overcommit behavior) with the mechanism used to return the error. Ironically, the “simply hidden” failure mode in the case of arithmetic operations describe a case where Rust would panic, so throw an exception.

                    build some more reliable interfaces that don’t panic, e.g. Box::try_new()

                    This interface is useful in a kernel context where you know that there is no overcommit, and completely useless in an application where the memory will be happily returned to you, and then the OOM killer will kill your process. Anyway, it has no bearing to exception vs result, as try_new returns a result, so the point being made is unclear.

                    Exceptions Lead to Better Error Messages

                    This paragraph is about the dilemma of putting context in the error type or fetching it from the environment. It applies all the same regardless of whether you’re using exceptions or results.

                    Exceptions Are More Performant

                    This is generally mitigated a lot in the presence of inlining. It is also more the characteristics of the current implementations that something that will necessarily remain true.

                    I’m still a bit puzzled as to why newer languages like Rust or Go don’t allow the use of exceptions.

                    Rust does, in the form of panics. They do occupy some space in the error-handling story, taking in on some use cases where they are a better fit than Result (e.g. asserts)

                    1. 2

                      Rust does, in the form of panics. They do occupy some space in the error-handling story, taking in on some use cases where they are a better fit than Result (e.g. asserts)

                      Go also has panics, and AFAIK they’re also used in those situations (e.g.: OOM). So yes, while I prefer Exceptions than Errors, I also can’t concur with the author points.

                    2. 19

                      The article throws C’s -1, golang’s and C++ verbosity, frowned-upon Rust practices, and OOM killer into one pot. This is all over the place.

                      Technical many languages “return error values”, but the problems you get from ancient C/POSIX APIs are not the problems of Result types.

                      Rust’s standard library has unfortunately assumed that out-of-memory errors are impossible to detect (Rust devs are Linux-centric), but that has no impact on handling of all other kinds of application errors that do happen.

                      If some operation can fail, and you’ve made an infallible function, then having to change the API (or handle the error and recover) is the whole point of having failures explicit in the type system, so that callers need to consciously decide whether to handle or propagate the error. The common case is just a single ? char, shorter than .unwrap().

                      1. 9

                        This is the second article from the same group of people that throws shade on Rust (the first one was about German Strings). Given that they are making a database, I really don’t understand why is that necessary? Are they trying to defend their choice of language?

                        Agree with you that the article is all over the place.

                        1. 3

                          This is the second article from the same group of people that throws shade on Rust (the first one was about German Strings).

                          For reference: https://lobste.rs/s/jouh8m/why_german_strings_are_everywhere

                          In particular, that “shade” was incorrect, in a way that a Google search could have easily shown them.

                          This string implementation also allows for the very important “short string optimization”: A short enough string can be stored “in place”, i.e., we set a specific bit in the capacity field and the remainder of capacity as well as size and ptr become the string itself. This way we save on allocating a buffer and a pointer dereference each time we access the string. An optimization that’s impossible in Rust, by the way ;)².

                          1. EDIT 04/09/2024: This small aside lead to a lot of discussion in the Rust community: To clarify, the implementation and API guarantees of Rust’s std::string::String make such optimization impossible. It is, however, very possible to implement German style strings in Rust, as a lot of very impressive crates inspired by our snide remark have proven. Here are a few examples: An Optimization That’s Impossible in Rust!, An (extremely unsafe) experiment in implementing “German strings” in Rust “An optimization that’s impossible in Rust”

                          Their edit omits that there were already numerous libraries providing small-string-optimized string types in Rust when they made that “snide remark”.

                      2. 9

                        Exceptions provide separation of concerns by keeping the error path distinct

                        Well, this is the thing, isn’t it? With exceptions, there isn’t “the” error path. Whenever an exception is raised, there are any number of paths, and which one is taken is determined dynamically by the call stack and the catch behavior of intervening functions, which is one of those hidden drawbacks that @dureuill mentions. I don’t really like cyclomatic complexity as a metric to target for optimization, but I think it’s a useful way to think about how code is written, and exceptions make it exceedingly difficult to determine. To my mind, then, that bit with grepping for lines of error handling demonstrates exactly the opposite of what the author is trying to assert: the low count of lines of exception handling is a sort of deafening silence, since those error paths don’t go away, they just cease to announce their presence.

                        This is less of a technical point, but also I think many of the people involved in creating these languages are informed by their frustration with a particular method of (imo) abusing exceptions that has proven enduringly popular. That is: writing partial functions (that are otherwise referentially transparent over their domains of definition) and raising exceptions when the input is outside that domain. In my experience, it is much more pleasant to write the same logic as a total function where the output type is lifted into something like an Option or a Result, so now I don’t have to go into exception world and all of its inevitable dynamism when in fact I can have real referential transparency and all the benefits that come with it. In a language like Java that lacks sum types (prior to the introduction of sealed classes) or, I guess, multiple return values, this use of exceptions is nearly inevitable, but it’s nonetheless a bummer and there’s a relief in knowing that it’s not possible (barring panicking when the input is outside the domain of definition, but realistically doing so is clearly going against the grain of the language that it just doesn’t happen).

                        1. 3

                          With exceptions, the control flow is simple: when an error happens, you jump out to the closest place that can handle it. With result types, error handling is mixed with the handling of non-errror cases, making the control flow more difficult.

                          1. 6

                            Simple to explain in words, maybe, but much more complicated in terms of the potential control flows that result and the states the program could be in. Explicit and implicit are not the same as complicated and simple, and quite often are the opposite.

                            1. 3

                              It’s for all practical purposes a conditional and a return (a single caller up), that might recursively do more conditional returns. It’s absolutely bog-standard control flow, nothing like gotos, so I very much question the “much more complexity” part. It’s not even about explicit or implicit — exceptions are just as explicit when they handle errors. They just have a sane default behavior, which could be trivial syntactic sugar only.

                              1. 5

                                How does it work in the presence of concurrency and multiple threads? How does it work in the presense of resource usages and is it possible to do clean handling of resources, including closing them properly? What happens if an exception is thrown during the cleanup?

                                It might be “simple” to implement exceptions, but their intuitive handling is a different beast altogether.

                                exceptions are just as explicit when they handle errors

                                That’s also not really true. With errors, you can immediately see from a function if it possibly returns an error. This is impossible to do with exceptions. Which makes one of their strengths but at the same time it’s also a weakness.

                                1. 2

                                  This is impossible to do with exceptions.

                                  It is possible in languages that support exception tracking, such as Nim.

                                  1. 1

                                    Interesting!

                                  2. 2

                                    Threads also have a call stack, so that’s how it works. Concurrency makes a lot of things difficult, but one can design an intuitive logic here.

                                    Resource usage? What do you mean, like open files? Try-with-resources are the only structure that does the correct thing by far, see the recent post on the orange site where go’s error handling was found faulty (again) with defer and file close.

                                    immediately see from a function..

                                    Checked exceptions have been here for 20+ years, and effect types are a recent development that solves this issue. This is absolutely not an inherent property of exceptions. One can return either an error or a valid result in a dynamic language as well, the fundamental error handling semantics doesn’t depend on the function signature telling so or not.

                                    1. 1

                                      Resource usage? What do you mean, like open files? Try-with-resources are the only structure that does the correct thing by far,

                                      Zig’s defer/errdefer does exceptionally well, and often more than can be expressed with a “try with”-type arrangement, like “ensure we close this file handle/free this memory/enqueue a cleanup task for this if we return abnormally, but not on a normal return (because we’re returning a struct that holds it, or we’ve put it into a contextual/global map somewhere, or whatever)”.

                                      see the recent post on the orange site where go’s error handling was found faulty (again) with defer and file close.

                                      Can you clarify which post? There’s been about sixty in the last hour and I can’t find it.

                                        1. 1

                                          Ta! In this context, Zig doesn’t allow silently throwing away a return value like that, so it must be explicit (defer _ = x.y()); the standard library defines File.close to not signal errors (it’s void), and that an explicit fsync must be done prior to close if you want to get equivalent information.

                                        2. 1

                                          https://news.ycombinator.com/item?id=41479283

                                          Zig definitely has some very welcome new “syntactic sugars” around error handling.

                                          1. 1

                                            Ta! And indeed — they make a big difference, and there’s quite a bit more to it than syntax sugar given how errors are structured in the language. (Put another way, it’s the only language at the level of C to give you enough expressiveness to do cleanup correctly all the time without it being a huge hassle.)

                                            1. 1

                                              Absolutely, I really like their union-y error types! Of course it is not a general solution to every use case, as I believe it only works when every part of the program is known ahead, but for the niche it targets its perfect!

                                              My only gripe is their insistence on “unused variable checking” and such..

                                              1. 1

                                                Of course it is not a general solution to every use case, as I believe it only works when every part of the program is known ahead, but for the niche it targets its perfect!

                                                Maybe! I’m not sure exactly what you refer to, but to give an example, a common idiom is to define a generic function’s return type (including the error) in terms of the type it takes, such that:

                                                fn usesAThing(
                                                    comptime T: type,
                                                    allocator: Allocator,
                                                    blah: *const T,
                                                ) (Allocator.Error || T.Error)!void {
                                                    // does something which allocates and uses something on
                                                    // `blah`; could return an allocator error, or an error as
                                                    // defined by T, whatever that may be (if any).
                                                }
                                                

                                                This is e.g. how generic readers and writers work, where some may need to include allocation errors, some I/O errors, etc. — the called (library) code can take the lead from the caller.

                                                1. 2

                                                  I am absolutely out of my depth here, but I remember reading that error types get assigned a unique, small number, so that returning errors can still fit into 64-bit or so? But this also mean that the used numbers for these errors have to be known ahead of time, and that there is a limit on how many different types you can have.

                                                  1. 2

                                                    Indeed, there’s no Zig ABI, as such — I guess introducing yet another one doesn’t seem helpful when there’s extensive support for using the C ABI (both as a consumer and producer) — so knowing it “ahead of time” isn’t really a thing, as at compile time you know every error that’s referenced by reachable code. Zig libraries aren’t distributed in binary form, nor could they generally be, given comptime.

                                                    The default error set type is u16, but you can override it if that turns out to be insufficient.

                                        3. 1

                                          Resource usage? What do you mean, like open files? Try-with-resources are the only structure that does the correct thing by far

                                          I totally disagree. Zig’s defer is a superior example. I personally prefer how this is handled by libraries like Scala’s ZIO (“scoped”) or even cats (“Resource”).

                                          For instance: what happens if you have a custom resource and an exception is thrown both in the try-part as well as by the close method of the resource? Do you know how that will behave without trying it out or reading it up?

                                          Or what happens if there are multiple / nested resources declared and the same as above happens?

                                          Not intuitive at all imho.

                                      1. 2

                                        I think you can locally translate goto to throw and catch, so it’s not “nothing like goto”, imo. Anyway I think the point is that a function usually has a handful of returns in total, while with exceptions it could easily have more than one “return” per line, and you can’t see what causes them or where they go without exploring the entire call graph, since exception handlers are dynamically scoped.

                                        1. 2

                                          In fact Modula-3 defined loop EXIT (ie break) and RETURN in terms of exceptions tho it did not have goto as such. You need help from a loop to desugar a backwards goto into exceptions.

                                          1. 2

                                            I don’t think that’s right. You can only jump to places that are part of the given call stack (at least in the most common implementations - I’m sure ruby or some other very dynamic language can do some “hacks”).

                                            The “dynamic scoping” also seems a bit extreme, the call graph is for the most part statically known. Even with function pointers/lambdas they can be traced back to a call location with a ctrl+click in most IDEs, as the function symbol is usually also statically known.

                                            And the mechanism of exceptions doesn’t mean they can’t be part of the type system (effect types), or some other mechanism (checked exceptions) to notify us of something potentially throwing.

                                            1. 1

                                              You can only jump to places that are part of the given call stack

                                              goto can also only jump to places that are part of the current call stack, because it can only jump elsewhere in the current invocation. (I’m aware of the other goto but I started programming long after it was a mainstream thing).

                                              The “dynamic scoping” also seems a bit extreme, the call graph is for the most part statically known.

                                              One function can be called from different places—that’s sort of the point of them—and going through all the places an exception might land you is tedious work that’s pretty much always left to the human. Dynamic scoping doesn’t imply the call graph is unknowable, just that you have to look at everything in it to know what might be in scope. Although since you mention it, there are plenty of cases out there of dynamism defeating IDEs, or even determined humans. Python programmers really like building method names up from strings and calling them, Java programmers are required by international law to do everything through a mess of reflection and proxy objects 200 calls deep, etc…

                                              And the mechanism of exceptions doesn’t mean they can’t be part of the type system (effect types), or some other mechanism (checked exceptions) to notify us of something potentially throwing.

                                              That’s true but it has a history of going poorly (see Java…); I’m just thinking out loud here, but maybe because it runs counter to the defining feature of exceptions? Like they bubble up, ignoring oblivious intermediate layers and exposing the proximate cause directly to the eventual handler… except all the oblivious intermediate layers need to signify their acceptance of this now, and carry it with them everywhere they go.

                                              1. 2

                                                I’m aware of the other goto but I started programming long after it was a mainstream thing

                                                Fair enough, I’m also not that long into programming, but for some reason I defaulted to that meaning.

                                                Regarding the multiple possible invocation points.. I think it is a bit of a different philosophy here. In my view it is the caller’s responsibility to properly handle the error cases, this is not a local property of the program. So of course there will be different unhappy paths/strategies depending on which call tree we are at. There is just not much point in asking whether this function’s error case is properly handled everywhere, the same way we don’t ask what happens at the place someone plugs (0,0) into x/y at the definition of the expression.

                                                except all the oblivious intermediate layers need to signify their acceptance of this now, and carry it with them everywhere they go

                                                This is actually solved by effect type systems, where you can be parametric in “throwingness”, e.g. a map function can have a signature that it throws if the passed in function throws, but is not throwing otherwise.

                                      2. 6

                                        Raymond Chen had a post around 20 years ago pointing out the exact opposite. Without exceptions, control flow a call has no intraprocedural flow control. A basic block can contain a sequence of calls and the compiler (and programmer) can reason about this as if it were a sequential block of code. In contrast, with exceptions, each call has to be modelled as a conditional branch (LLVM has an invoke instruction to model this). This makes your control-flow graph vastly more complicated in any function that calls functions that can throw. It impacts variable liveness and the optimisations that you can perform (for example, you can’t move a store across a call that may throw if it dominates a load that happens after the catch).

                                        1. 1

                                          This is the exact rationale, too, for syntactically explicit ways of simply forwarding an error when the local context doesn’t want to do anything specific about it (i.e. Rust ? operator, Zig try) — the programmer knows which lines can possibly jump out, and short of a full panic, the rest simply cannot. It’s a freeing feeling!

                                    2. 7

                                      I think there’s room for both. If Rust were a different language it would be a lot more reasonable for it to add proper exceptions - not panics, but actually typed exceptions.

                                      This means that I prioritize keeping the server process running, and would rather have a single request fail than the entire application crash.

                                      This is interesting because I know a lot of people using Rust on the backend who explicitly want to always set panic=abort so that they don’t keep going when they encounter a bug, they’d rather the process starts clean.

                                      Still, I’m amazed that this is supposed to be the state of the art for systems programming. Heck, even Java handles this much better: Catch an OutOfMemoryError and apologize to one client instead of killing the whole server and interrupting service to thousands of clients.

                                      I guess perhaps it’s different when you’re a database? But generally if a server crashes I do not assume that I’ll interrupt anyone, let alone thousands of clients. Computers crash unexpectedly, I already have to think about keeping a system up if it goes down.

                                      We parse an int somewhere, and an IntErrorKind::InvalidDigit bubbles up at the user. Here I am, parsing megabytes of CSV, and you tell me “invalid digit found in string”.

                                      I think there’s two separate problems here.

                                      1. You should be adding context at the call site.

                                      2. Lack of backtraces is a problem. It’s probably Rust’s biggest problem today, imo.

                                      performance stuff

                                      Maybe. I wish they’d posted the Rust code so I could see. The way it should work is something like a jnz with the “Ok” path following that instruction, meaning that your icache is basically not impacted. It may not optimize out that way though. The size of values will definitely be larger too.

                                      Overall, I think it’s not so clear cut which approach is better. I’m ambivalent tbh. I think Rust’s error handling needs a lot of work (backtraces + easier composition of error types) but I don’t run into issues with it personally.

                                      1. 6

                                        Error handling strategies are about communication strategies. Different types of errors need different types of communication.

                                        Errors as Exceptions as used in the wild today:
                                        • Emphasize communicating with toplevel code like a framework.
                                        • Emphasize hiding information from middle code.
                                        Errors as values as used in the wild today
                                        • Emphasize communicating with the direct caller of the code.
                                        • Emphasize hiding error domain specifics from top level code.

                                        I think there are a few different classes of errors where different dimensions of communication are important.

                                        • Errors that the caller needs to be aware of. (Checked exceptions or Errors as Values are good here.) Because the type signature of the code tells me the errors I need to be aware of.

                                        • Errors that indicate this line of computation is borked and needs to be abandoned. (Runtime Exceptions are a sweet spot for this) Because as the caller I can’t reasonably do anything to recover and should leave it to the toplevel framework to handle. This error is not my concern so it shouldn’t pollute my type signature.

                                        • Errors that indicate everything is borked and you should just die so the OS or whatever is managing you can take appropriate action. (Runtime exceptions are good here.) Because again as the caller I can’t reasonably do anything to handle this and need to leave it up to something else above me. This error is also not my concern so it shouldn’t pollute my type signature.

                                        Most of the my error handling problems that I deal with in wild are of the first sort. Framework code generally handles all of the cases of 2 and 3. Which means my day to day code is often concerned with code paths where errors as values are a better fit.

                                        If I’m a framework developer I care a lot about the second one. and maybe a little about the third one. But usually I’m not writing the framework. I’m writing the code that uses that framework.

                                        1. 5

                                          I think we will finally get the best from both worlds when more languages start implement “effect handlers” or “type sets” like in Effekt. You use them like exceptions but they provide the same type safety of using error values.

                                          1. 4

                                            Of course, there are solutions to reduce the amount of boilerplate. In Rust, you can sugar coat the ugly syntax with the ? macro. This just hides all the tedious checking, but you still need to do all the work. Additionally, your function now returns error codes, which means you have to change the public interface and recursively change all functions that call this function.

                                            I’m not sure what the “you still need to do all the work” refers to here, but one issue I’ve had with Rust (not sure if that has changed recently) is that errors don’t compose: Say you have two APIs you integrate with, and both define an error enum to cover all the cases that can happen, then you’re forced to transform these two types into one somehow if you want to call one after the other and use the ? operator. I know it’s possible to use some Into trick here, but it still requires a new datatype and some code to map from one enum option to another.

                                            OCaml, on the other hand, has composable error handling via polymorphic variants. Since I’m not currently writing any type signatures, this is fine for me, but I suspect it becomes tedious if I were to create .mli signatures. That being said, it seems OCaml libraries still very much prefers exceptions, as I’ve had to wrap a lot of them in e.g. JSON parsing and whatnot. Roc seems to lean completely into the polymorphic variants error handling, which I very much love. But from what I can tell, still has the problem of keeping these types up-to-date if I were to add a new error variant down the stack, and also want to manually write my type signatures.

                                            … which I guess is a long-winded way of saying “yes, there’s a bit of overhead for the developer”, but it’s not as bad as it once was, and I think errors should be in your face. As for performance.. maybe? Not that its performance doesn’t matter, but your bottleneck surely lies elsewhere.

                                            1. 4

                                              errors don’t compose: Say you have two APIs you integrate with, and both define an error enum to cover all the cases that can happen, then you’re forced to transform these two types into one somehow if you want to call one after the other and use the ? operator

                                              If you don’t care about preserving the exact semantic of the error, you could use a trait object return type of Box<dyn std::error::Error>. Or, you’d have to be explicit and define your own errors.

                                              I know it’s possible to use some Into trick here, but it still requires a new datatype and some code to map from one enum option to another.

                                              Another option is just a newtype wrapper that implements Error and wraps those two enums of error for the cases you care about, like

                                              enum MyError {
                                                  Crate1Error(TheirError1),
                                                  Crate2Error(TheirError2),
                                              }
                                              
                                              1. 2

                                                Yeah, the latter is what I’m familiar with. But it causes a little bit of messy nesting imo, and it’s less granular than I’d like it to be. Assume, for example, that I handle all error types of a particular kind from TheirError1 – I still have to pattern match on it as a consumer, unless I wrap every other error.

                                                1. 1

                                                  Yes, that is true. I’m hoping this becomes easier/ergonomic with Try trait stabilization.

                                              2. 1

                                                The composable error issue is interesting to me. I think a weakness-that-is-a-strength of Go is that errors are not exhaustive enums, but just an open ended interface, so that caller knows that they probably have to be prepared to handle unknown error types as well as known types.

                                                1. 1

                                                  In application-level code consider using e.g. anyhow or eyre, they are a much more powerful alternative to Box<dyn std::error::Error> and make error handling easy if the exact error type is not needed. (eyre has some additional niceties e.g. integrating with tracing via the tracing_error crate)

                                                2. 4

                                                  I prefer Result types as much as possible, even using them where they aren’t the most idiomatic (Kotlin).

                                                  But exceptions also feel like a casuality of poor language implementations (C++ performance issues, Java checked exception mess) coinciding with general industry sentiment of “don’t make me think too hard,” and are often conflated with OOP. They also make writing code more difficult because you have to account for forward control flow and exceptional control flow. Writing code that interfaces with multiple fallible systems means sitting there thinking, “well, what if system2’s update fails and we already wrote to system1 and still have to write to system3?” You have to inject additional state to indicate how far along you were, and how many actions you need to reverse/undo.

                                                  With errors as values, those cases become more clear. Thus, the pit of success is not as far. Of course, there’s plenty of ways to make unforced errors here, but I consider it an improvement on exceptions.

                                                  Also, lots of language implementations offer little help in determining what can actually be thrown by a function. I find this infuriating; it should be part of the calling contract.

                                                  1. 3

                                                    C++ performance issues

                                                    There are two sources of performance issues from exceptions, one is intrinsic and the other is an implementation choice. The intrinsic property is that each call now adds (at least) one more arc in a function’s CFG. This impedes a lot of other optimisations. In terms of implementation choices, there are basically three ways of implementing exceptions:

                                                    The original choice is largely unused now. Exceptions were implemented by calling setjmp in the start of each try block and maintaining a (thread-local) linked list of jump buffers, where each jump buffer is on the stack and the head is in a thread-local global. This was used by SEH on Win32, by Objective-C before the language got native exceptions, and by GCC as a fallback for platforms that can’t fully support Itanium-style unwinding. The main downside is that setjmp plus a linked-list insertion is quite expensive.

                                                    Most C++ implementations (and SEH on Win64) use some form of table-based unwinder. When an exception is thrown, the unwinder needs to find the table associated with the current program counter, then interpret a set of instructions for how to unwind the frame. It needs to then do this again for the PC that it finds after recovering a return address. This is slow, but has the benefit that it doesn’t add any overhead when exceptions are not thrown (unlike setjmp).

                                                    The third option is to lower the calls to something equivalent to an option type, where each call is followed explicitly by a branch. I’d really like to try implementing this using something like the FreeBSD system call convention. FreeBSD returns either success values or error values in a return register and uses the carry flag register to differentiate between the two. This lets you do a branch-on-carry in the return to go to the slow path that sets the errno global in userspace. Branch-on-carry is statically predicted not taken on pretty much any implementation, so this should be close to free for the not-throwing case (small increase in code size / i-cache usage), but still quite cheap (a pipeline flush) for the throwing case.

                                                    Java checked exception mess

                                                    Checked exceptions are always a bit of a problem, but so are unchecked ones. If you have unchecked exceptions, you have to assume that any function can throw any exception. This is more or less the case in C++ and it’s painful. It was improved by adding noexcept, so at least some functions can be marked as not throwing at all. In contrast, if you have checked exceptions then the set of exceptions that you can throw is part of your signature and you cannot change it, except by adding new subtypes of existing advertised exceptions.

                                                    1. 2

                                                      the unwinder needs to find the table associated with the current program counter, then interpret a set of instructions for how to unwind the frame

                                                      I am surprised the unwind code isn’t compiled!

                                                      1. 2

                                                        It’s partly down to code size. DWARF unwind tables are very dense. More than that though, they depend on the unwinder’s internal data structures. For example, an unwind table instruction may say ‘the value for register 4 is stored at offset 16 from the stack pointer’. An instruction that does a sp-relative load into register 4 is easy in most instruction sets, but that’s not actually what the unwinder is doing: it’s building a copy of the register file that it can then do something equivalent to setcontext with. You don’t want to bake that into generated code.

                                                        I’m not sure about Win64, but on DWARF-unwinder platforms the time spent executing the bytecode is largely in the noise relative to the time spent finding the unwind table for a function.

                                                        There’s a lot of stuff in DWARF unwinding that most compilers don’t use. For example, if you have an array value that you then iterate over with a loop and need the original of in a catch block, there’s enough in DWARF unwind that you can rematerialise the original by subtracting a value computed from the loop induction variable. Unfortunately, determining which operations are destructive is sufficiently hard that you’ll typically just spill the original and reload it.

                                                  2. 3

                                                    A Rust version is slightly faster than the C++ expected version, but still 4× slower than the throwing version

                                                    would you mind sharing your rust benchmarked code?

                                                    1. 3

                                                      Am I the only one who is bothered that the font size in the code examples seems to vary randomly, e.g. the “throw” line in the first example is larger than the other lines? I have seen this in some other blogs as well, recently.

                                                      1. 1

                                                        Are you on iOS? I’ve been seeing this quite often in code samples on my iPhone. But never on desktop (Firefox). My guess is that there’s some “smart” readability feature on Safari that is messing things up.

                                                        1. 1

                                                          I‘m on an iPhone and it happens both in Safari and Firefox. Truly weird.

                                                          1. 1

                                                            I think the CSS needs to set these (can’t remember if both are required) for the preformatted code blocks to prevent this

                                                            text-size-adjust: none;
                                                            -webkit-text-size-adjust: none;
                                                            

                                                            Probably in a code or pre selector

                                                        2. 3

                                                          In cases like this when the article is upvoted but the vast majority of top level comments disagree I wonder why. Do people disagree but also upvote because of some reason? I can understand not wanting to be the lone person to speak up for liking a thing the crowd/mob appears to not like. Ultimately it feels like there’s a perspective here that’s not represented.

                                                          1. 9

                                                            Do people disagree but also upvote because of some reason?

                                                            … Yes? The upvote button isn’t an agree button.

                                                            Also people who agree with the article don’t need to repeat its arguments, and comments that just say “I agree with this” are discouraged.

                                                            1. 7

                                                              I upvoted this article even though I myself think that Rust-style error value types are generally preferable to exceptions. It made some good arguments for things exceptions do better than error values, and pointed out some flaws with the way error values are implemented in a variety of languages. That counter-argument is valuable even if you ultimately still prefer value types.

                                                              Also, bringing up these arguments invites people who are fans of error values to think of ways error values could solve some of the mentioned problems (e.g. I’ve used rust error libraries that manually build up a stack trace so it can be printed when a top-level loop handles the error, basically mimicking what a language with a runtime might do when it encounters an exception - maybe this sort of thing could be made more ergonomic or built into the standard library of some new language). And this comment suggested that maybe effect systems could be used to get the good points of both exceptions and error handling, which is an interesting idea sparked by this article.

                                                            2. 2

                                                              Swift has it both ways, and in my view tightens up the exception experience from 90s languages:

                                                              • Any type you want can be conformed to Error. It’s a protocol with no requirements.
                                                              • We have a Result type. Its second type parameter must conform to Error.
                                                              • Much like a function can be async, it can also be throws(YourType) or just throws, which since recently is shorthand for throws(any Error). In such a function, you can throw values of that type.
                                                              • Much like a call to an async function must have await in front of the expression, so you can see the suspension point, a call to a throwing function must have try in front of the expression, so you can see the potential jump. That means you know all other code executes in a straight line.

                                                              More details in the Swift book.

                                                              Terminology notes: do {} stands in for what try {} means in other languages. The word exception means the process-level kind, like a segfault.

                                                              History: Before Swift, the pattern was for a function to take an NSError ** and populate it in error cases. The SDK functions that worked this way were all interop-translated into throwing functions for Swift, so throwing became idiomatic. When we got a feel for sum types, some of us started using results a lot in callback functions or promises. The arrival of async/await meant we could go back to throw for error cases, especially since async and throws compose neatly.

                                                              1. 2

                                                                My point of view is that error values are the most natural way of fitting failed actions in type systems. They are explicit and need to be handled or at least unwrapped. Unwrapping and/or propagating can be annoying.

                                                                Exceptions bypass the normal type system. They make it far harder to reason about the system, but make it easier to propagate errors, and to write quick & dirty scripts that mostly work.

                                                                1. 2

                                                                  Oh damn, these people are making a database? 😬

                                                                  1. 2

                                                                    I think this post somewhat conflated semantics and implementation. There is no reason why Go-style exceptions couldn’t be implemented via tagged unions, and there’s no reason why Rust-style result types couldn’t be implemented with stack unwinding (I seem to recall boats proposing such a thing a few years ago!).

                                                                    1. 1

                                                                      Except for the performance part, perhaps monadic error handling with error values might be what you are looking for? You get most of the features of exceptions (throw from anywhere, better error messages etc.) with the error values.

                                                                      1. 2

                                                                        I’d say that rust’s ? operator is a specialized form of monadic bind and return over Result, which is compared against in the article.

                                                                        1. 3

                                                                          rust’s ? operator is a specialized form of monadic bind and return over Result

                                                                          It’s not quite so specialized: it’s implementable (unstably) for any similar type and implemented (even stably) for more than just Result.