54 comments

[ 3.2 ms ] story [ 98.0 ms ] thread
Linux wants to be a 1970's mainframe.

TempleOS wants to be a modern C64.

In 1970's they had 100 users on 2 Meg of RAM.

Today, nobody would tolerate how slow disk swapping is, if you ever actually rand out of RAM and started swapping.

Well, that's one position on the subject. The Rust people prefer option 3 over option 4. Go takes the same approach. Python prefers exceptions, and the exception hierarchy puts (almost) all the exceptions which result from external problems under EnvironmentError.

Much of the trouble with error returns comes from the strange C convention that functions with return values can be called as if they didn't return a value. This is a consequence of the original K&R PDP-11 implementation without function prototypes, and has messed up programmers for four decades now.

RAII (resource acquisition is initialization) is popular in C++, but if anything goes wrong in a destructor, things usually come unglued. I/O errors on file close where close is via a destructor tend to be either ignored or fatal. Python "with" clauses handle failure during release better. This was well thought out in Python; exceptions in "with" clause exits are handled rationally.

With Javascript, you get none of the above, although you can build it out of the primitives, and there are kludges to do this.

I also prefer option 3 over option 4. It bothers me that the article recommends exceptions without seeming to understand their drawbacks (the major one: as soon as you use exceptions you suddenly need to apply nonlocal reasoning everywhere in order to understand what your program will do at any point. They turn your program from a simple local thing into a complex nonlocal thing, which is not a good idea if you want to understand it well.)

This article seems not to have a lot in the way of new contribution; it is just parroting the oft-repeated idea that you need exceptions to pass "error information" up several layers of abstraction. But here is what I think about this:

1) In this age when we are realizing strong typing is a good idea, that hidden state is a bad idea, and that in general you should be very specific about what is going on, why are we even conceptualizing this as "error information"? Why instead, when we try to open a file, do we not return "all information you might need to know in the case of opening a file" (which includes what happened if it didn't open properly). As soon as you make that conceptual switch, all this hand-wringing goes away. It's a non-problem. You certainly shouldn't add heinous complications to your program to solve this non-problem.

1a) This conceptual change also helps disambiguate between what the article calls "hard errors" and "soft errors". In portions of the code where you have attempted an operation that might have failed, and you are not completely sure that it didn't fail, you have the full body of "what happened" information (it is a small struct or whatever). After the situation has been checked and you know it is exactly what you need to be, you may drop the other information and pass the raw file handle. At this point it is clear that these parts of the code should only be executed if the file handle is valid, and if that is not true, the programmer made an error. This is analogous to the situation with nullable and non-nullable pointers (in some languages you would even use the same mechanisms to deal with null pointers and invalid file handles, etc, but I am not sure this is really helpful.)

2) If one insists on not making the simplifying leap from (1), well, maybe the other problem is that you have so many layers. If you didn't have so much glue, your code would be simpler and easier to deal with, and it would run faster, and you wouldn't be so worried about needing to pass lots of context information up several layers between modules, because those situations don't really arise.

In this age when we are realizing strong typing is a good idea, that hidden state is a bad idea, and that in general you should be very specific about what is going on

Java tried to be typesafe about errors, about not obscuring the state, and it was a dreadful idea. The way code fails is a function of its implementation; communicating high quality error information is fundamentally an abstraction violation, but a strongly typed facade that forces you to rewrap all your error cases in some other type just obscures the underlying problem.

Strongly typed error information also breaks composability. How do you write a 'map' function that can accept a callback, where the callback may want to communicate back one of several distinct kinds of error information? With unchecked exceptions, the way forward is clear. Without them, you either need to write a lot of boilerplate to unify your error types in some kind of container, or rely on subtyping to fit them into a single type and live with the lack of strong typing via another route.

Dynamically typed programming languages improve usability over statically typed languages particularly when a precise description of the types used in the static case is difficult or tedious to express. Error information, and its intrinsic implementation-dependent content, is one of the strongest instances of this.

You could have checked exceptions with more flexibility than Java had, and it might work out well (or might break down for other reasons).

"How do you write a 'map' function that can accept a callback, where the callback may want to communicate back one of several distinct kinds of error?"

With Java style checked exceptions, you don't, and that's for sure a problem.

However, one approach would be to say that map is polymorphic in the exceptions that may be raised from within it. But that set is not entirely unbounded - it is precisely the set that can be raised by its function argument, unioned with any that might be raised during traversal of its container argument.

Exceptions are hard to get right in language design. They're easier in a garbage-collected language like Python, because there's not so much need to release stuff. Most of the bad reputation of exceptions comes from C++, where the combination of exceptions, RAII, and having to release memory results in problems.

Without exceptions, programs seem to develop too many "goto" statements. Go suffers in this way. Error handling in Rust seems to excessively complex, and hiding the complexity inside macros that do return statements is an ugly solution to the problem.

For my own understanding, why do you say "(Exceptions) turn your program from a simple local thing into a complex nonlocal thing, which is not a good idea if you want to understand it well."?

My understanding exceptions only bubble up if they weren't handled at the point of failure. This is exactly the same if you don't check the return type of a function that could return error. Both of this situations point to poor programming technique rather than underlying implementation option.

One thing that makes me currently prefer exceptions to error returns is that within a try/with block you can write the code as you'd like to happen and hence easier to understand and maintain. All exception handling can happen within the respective catch blocks.

Because in order to know which instructions in your procedure might excecute, you need to know about the exception-handling behavior for everyone you call, which requires looking at the source code of everyone you call. User barkkel above in this thread was saying that passing around full return-value information for something like a file-open operation is somehow a violation of abstraction (this idea does not make sense to me) ... But I can't think of a bigger violation of abstraction than requiring you to know everything about everyone you ever call. Of course in reality people don't do this, which is then why programs that use exceptions have so many problems.

And if you say "why would you let exceptions bubble up that much", well, that is the whole point of exceptions, that they bubble up. If you say "to get rid of nonlocality just catch outside every call", well, now that's equivalent to checking return values always, but more error-prone.

Strong type system with a good support of checked exceptions would tell you what kind of exceptions can arise from every function call. If you don't check them in your function code, they would add to the list of exceptions that your function can throw. It would basically turn every function you write with return type T into Either<T, ExceptionA, ExceptionC, ExceptionH> with syntax sugar that would transfer exceptions between calls so you don't have to spend your time with constant transfering of error properties of your return objects.

This would allow you to check stuff on places you desire (sometimes right after function, sometimes in the UI thread) and have tooling support for managing what is left unchecked.

Let's build something like that for C# using Roslyn! Issues of Java-style checked exceptions can be overcome using proper typing with generics.

With this scheme you would end up with pretty bad problems regarding function pointers and lambdas. Because the type is deeply implicit, you would have two function pointers that look compatible but are totally incompatible. Then when you want to assign them to a variable -- how do you know a priori what type to declare the variable?
This would seem to be "just" a matter of correctly handling polymorphism along yet another axis.

Let's say we had the following C++ code:

    int foo(int (*bar)(char), int (*baz)(), char c) {
        try {
            return bar(c)
        } catch(SomeException &e) {
            raise otherexception;
        }
    }
What exceptions can be raised by foo?

The answer is simply computed - "anything raised by its first argument except SomeException, plus the type of otherexception".

Such a system would be tremendously more flexible than Java's checked exceptions, while still allowing you to confidently restrict what might be thrown in a section of code.

Good question. :-) I guess there definitely would be cases where the programmer would be restricted in what they want to do -- this is a nature of type systems. For example, I guess following code would not compile:

var f = (n) => n / 5; f = (n) => 5 / n;

What I hope for is that usual patterns would survive in a convenient manner. For example, calling function with function argument that is evaluated inside this function is something that could be handled by language without too many problems.

In layered code, if not recoverable by the current layer, you should always encapsulate checked exceptions thrown by a lower layer - translated into an exception of the current layer (maybe generalized). This simple rule is often declared as overhead. I don't get that. I like code to be precise. The only situations where this gets complicated are those, where the rule is violated. And I think, the rejection of checked exceptions is often based on experiences with bad API design.

Exception handling must be trained and educated. There is no silver bullet.

Go's preference for 3 over 4 is a big chunk of the reason I've never used it in anger. Rust's situation is slightly different; code typically uses a monad (Result<T,E>) for error propagation, which is mostly isomorphic to exception throwing, but more verbose. And Rust has macros to cope with some of the verbosity.
It may be isomorphic to exception handling, but it promotes a meaningfully-different human style. The exception-based isomorphism to the usual returned-error handling style would look something like:

    try:
        a_single_statement_here()
    except:
        if exception.type == "A":
            #yada yada
        elsif exception.type == "B":
            #yada yada
        else:
            raise
Exceptions by contrast encourage putting a lot of statements together, and much less frequently checking for what the exception may be, and capturing fewer of them. The code snippet above is bad exception style because you shouldn't be putting a try around every statement like that.

Exceptions expressed in the error handling modality probably look something more like:

    val, err = func()
    goto EXCEPTION if err
    nextval, err = func2(val)
    goto EXCEPTION if err
    # etc etc, for the whole "try" block

    return goodval

    EXCEPTION: {
        if err.type == "A" { ... }
        if err.type == "B" { ... }
        return err
    }
which is, intriguingly, also very bad style for an error-returning language; the EXCEPTION block must be written excessively generically, when in fact the error handling may in fact care about the difference between whether the error came from func or func2, and may even want to take different actions (especially w.r.t. retrying). You can of course write those different actions into an exception block, but the style discourages that... your code starts to look messier (as nice as exceptions can be, they're syntactically pretty heavy, and also difficult to factor out the way that error-return handling can be factored out with monads and such (albeit not in Go so much)), it takes more work, so you're less inclined to do it.

It remains unclear to me what "the best" is. Error-returning code is, both in theory and in my practice, more correct, in terms of the programming style it affords; it turns out that in ways you won't see if you've only ever done exceptions-based code that your exception handlers are often incorrect, overly generic, and missing opportunities to intelligently handle bugs. The flip side is that all that extra handling is of course expensive, and it's not 100% clear that it's worth the expense, and exception code, while IMHO clearly generally less correct in both theory and practice, is equally clearly often "good enough".

I find myself wondering if some of the divisions of opinion come from different environments. In a conventional GUI program, I favor the exceptions; frankly so much stuff can go wrong that an enumeration of all possible failures is very difficult to produce, and your answer to almost all of them is likely to be identical ("scream an error to the user and hope they can fix it"). In a network server, I heavily favor the error returning paradigm; in my mature Go codebases that are network servers, I took a survey once and about 1/3rd of the lines that received an error immediately did something with them, other than simply passing them up the stack. Converting that to exceptions would actually lengthen the program and be a royal pain to deal with going forward.

I think it's more a product of application complexity. The deeper the waters below you, the less you can meaningfully do with errors triggered by our actions. The closer you are to the hard boundary with the outside world, the more easily you can enumerate error conditions and be complete.

A server connected to a network socket is very close to the wire protocol, and the potential error states are fairly well known and documented. Head up the abstraction stack deep into business logic and add a dose of third-party libraries and some high-level application structure (i.e. indirection and abstraction boundaries) and the world gets a lot more fuzzy.

I concur re GUI; I think this comes from the complexity of the GUI framework - "don't call us, we'll call you" - GUI code is typically a thin seam between two sets of framework call stacks.

I wouldn't advocate exceptions for a kernel, or a database server. The more code between you and the hardware, and the more code that you use that you don't control, the more I feel the need for exceptions to communicate error information. The higher up the stack you go, it's more likely that human intervention will be required to solve the problem - in a server situation, log the error and tell the user that support has been informed about the problem. Etc.

I like this distinction. It makes sense to me based on my experience, and as an extra bonus, explains the passion on both sides of the debate. If you don't have both kinds of experience, obviously one is strictly better than the other, it just varies as to which one that is. Both sides look at the other in horror and ask "Why are you trying to ruin my programs?", with good justification.
I think part of it is that when interacting with the outside world, error states are not an unexpected return value. Network cables get disconnected, packets get lost, hardware breaks, things time out, etc. There are inherent races because the external world is concurrent; you can't grab something and then be assured that nobody else will mess with it.

Higher up the stack, it's more likely you're dealing with an abstract object of some kind with no direct correspondence with the outside world. Exceptions are more likely to be an unexpected state, where unexpected means an unexplored area of the state space owing to an increased number of moving parts.

> Much of the trouble with error returns comes from the strange C convention that functions with return values can be called as if they didn't return a value.

The "warn_unused_result" GCC function attribute makes the compiler emit a warning when you do `func(something);`: https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attribute...

The problem is it needs to be applied to every function that returns a value. And I'm pretty sure you can't apply it to C library functions. Gcc seriously needs a global flag for this.
You might be surprised at the warnings you'd get. Did you know, for example, that printf returns a value? It's also a return value that rarely, if ever, tends to be useful.
As a joke, in case you are really paranoic about error handling, you might end up with this :-)

    int written = printf("Got here");

    if (written != sizeof("Got here") - 1) {
        written = fprintf(stderr, "Could not print my sacred message\n");

        if (written != sizeof("Could not print my sacred message\n") - 1) {
            exit(I_DONT_KNOW_WTF_IS_HAPENNING_HERE);
        }
    }
That sort of thing is actually a completely valid approach.

You made a small mistake, since fprintf and printf call the same underlying code, the error handling should be either log the error in any way still available, and then depending on the design of the whole program, exit or continue. If printf fails you know exactly what is going on or have to exit. ( And printf would be wrapped in a function if you plan to use it repeatedly. )

Yes, I did. But so far I haven't been regularly checking printf return value. :-( I wonder if checking the result of every printf or asserting it, depending on the circumstances, would reduce the amount of time spent on writing+debugging the code on the long run.
Most statically typed languages prefer 3 over 4. For the simple reason that it's easier for a compiler to verify that you've handled all the success and error cases.

For Haskell though there exists a library which allows the compiler to track exceptions in much the same way as normal values, and thus warn you when you fail to handle an exception in a particular code path.

The Haskell approach would be to use `Maybe` or `Either SomeError`, and have Functor/Applicative/Monad/etc. handle the error propagation. More experimental approaches are using Algebraic Effects, which are like resumable exceptions, tracked in the type system, with ambient handlers.

It's an elegant way to solve this problem, mentioned in the article:

> However returning error codes makes error propagation difficult. When there’s need to propagate the error up the stack several layers, every layer needs to make sure that they do it correctly... ideally most of the code along the way should be error agnostic and not really need to know about the details of the possible errors but still be able to propagate them up to the caller without a hitch.

Exceptions have another advantage not mentioned in the article: performance.

If you implement them as "zero cost exceptions", then there is no overhead for the successful path. Option 3 requires error checking even if you return successful, which can only be optimized away maybe if the compiler inlines and moves code around. Since Go and Rust favor static linking this might work. If you want to link libraries it does not.

I like to say "Rust has exceptions, you just can't catch them". If you use panic!() in Rust, it will unwind the stack, just like exceptions do. Destructors correspond to finally blocks in Java.
Also worth noting that "things come unglued" applies to panics in destructors in Rust the way it applies to exceptions in destructors in C++.
He talks about "soft" errors as if the only way to handle them is to propagate them up to the UI layer, displaying a message.

In many situations the error handling affects the code paths at a more profound level. Something might be considered an error in the callee, but at the same time it might be an acceptable result in the caller, for which a well-defined path exists. Throwing an exception in such situations is just disconnecting the faulting code from the error handling code.

The perspective is about layered code and reuse. So what is wrong with delegation? My file access library should not know that it is linked in an application with an UI. Throwing an exception just delegates the issue to the code which depends on me. Whether the IO exception is really a problem can only be decided by the user of my library.
And option number five, conditions + restarts, http://www.lispworks.com/documentation/lw61/CLHS/Body/09_a.h..., which is a bit like more formalised callbacks / exceptions depending on usage. Restarts are a great thing for both interactive (using a "continue" restart if e.g. something can be safely skipped) and programmatic usage (to customise an algorithm where you control from the outside whether a restart should be invoked in case of a "soft" exception/condition).
Exceptions only work well in managed languages like C#, Java, Python.

In C++ they don’t.

In C++, error codes FTW. On Windows you usually return HRESULT, call FAILED/SUCEEDED macros to check, call OS-provided FormatMessage API to format an error (the messages are of course localized). If you’re using some 3rd party library that uses its own error codes, it’s usually trivial to pack them in HRESULT when failed, check HRESULT facility when formatting.

In C++, exceptions have fatal disadvantage: they don’t work across modules. Two reasons: (1) C++ exceptions aren’t binary compatible across compilers (2) memory management isn’t compatible across compilers either, if some module has called new/malloc, the same module must call delete/free

Don't really agree, this seems too general? It's a matter of preference, what level you're working on and of what is possible. For example you're writing C++ which is going to be wrapped in a C-style api then yes you are going to need those error codes. If you are writing an application using some C++ api with well-defined exceptions then using them might lead to much nicer code. Consider some function in main() which takes care of a ton of initialization; I just happen to prefer

  try
  {
    MethodA();
    MethodB();
    MethodC();
  }
  catch( const FooException& e )
  {
    //show e
    return 1;
  }
  catch( const BarException& e )
  {
    //show e
    return 1;
  }
over

  auto resulta = MethodA(); 
  if( FAILED( resulta ) )
  {
    //show result
    return 1;
  }
  auto resultb = MethodB(); 
  if( FAILED( resultb ) )
  {
    //show result
    return 1;
  }
  auto resultc = MethodC(); 
  if( FAILED( resultc ) )
  {
    //show result
    return 1;
  }

they don’t work across modules

A problem which fades away if you build all code using the same compiler and build settings, which is a good idea anyway (there's not much which works across modules when mixing e.g. debug and release builds). I've written a lot of C++, and misbehaving across modules is like the one thing I didn't have problems with :]

1. Harder to maintain. Your colleague will change MethodB so it now throws an ExceptionB(), and your code will crash with unhandled exception = segfault. With error codes, your colleague may return any FAILED code she finds adequate. As long as you have a standard way of showing messages, the caller doesn’t need to change a single line.

2. You can greatly simplify your return codes example with a macro, e.g.

  #define CHECK( hr ) { HRESULT __hr = (hr); if( FAILED( __hr ) ) return __hr; }
Then you will be able to handle them in the upper level. Also, by changing this macro to slightly more complex variant you can very easily log failed operations (remember that __FILE__, __LINE__, #hr, etc.)

“if you build all code using the same compiler and build settings” — fine, what about using other people’s libraries you can’t or don’t want to compile? Like OS-provided API, CRT, middleware, etc? Also, what happens with your project code style consistency if you’ll decide to replace 3rd party component (that unlikely uses exceptions) with in-house, or vice versa?

P.S. I’ve written a lot of C++ as well, I’ve been developing commercial software since 2000. My experience tells me if you’re going to achieve good quality of your product, it’s seldom sufficient to just print messages to stdout or MessageBox an exception message. Depending on what exactly happened and what the code was doing when something failed, it may be necessary to do something more complex that needs rich execution context. Like write stuff to logs, send error details to the client over a network in the response, close some windows, invoke a caller-provided error handler, etc. You don’t have that context where you catch() them.

1. Fair point, though in actual code at a top level (which the example shown was intended to be - as I'm not advocating one must use exceptions everywhere) there would be a catch( const std::exception& ) so it is not an issue.

2. Indeed you can

what about using other people’s libraries you can’t or don’t want to compile? I do want to compile them, or if I can't then either it is not a problem (at least I never had problems with OS provided APIs and CRTs which would give such problems because they are designed to circumvent it) or I might not want to use the libraries (never occurred to me, maybe I'm lacking experience or the particular applications I wrote just never gave me the chance of having to be in such situation).

Also, what happens with your project code style not really an argument, same can be said for switching to a library which does use exceptions

regarding PS: most of my/our applications have so much logging the lack of execution context is usually not a problem. In addition to that in those rare cases where things go completely berserk on Windows we'd use full minidumps. Not sure if that is what you mean with execution context though as I don't see how logging/sending error details/other things you mention would be handled differently when using exceptions vs error codes.

Go read one of Herb Sutter's "Exceptional C++" books. Okay, just read the first one. If you don't close the book and reflect, "I am never writing this shit," Herb didn't do his job.

Writing exception-safe code in C++ is very hard.

Depending on another module's authors to get their C++ exception handling right is the road to madness.

In the example above, the FAILED clauses clearly handle all the failures. In the exception handling example it is not clear if all the failures are caught, or if they are more that are meant to be caught at higher levels, or perhaps exceptions were forgotten. It becomes very difficult to reason about control flow because exceptions distribute it across the program and over time. Someone might add another exception: Oops, you didn't handle FileNotFoundException and your program bombs. You add a new exception that seems reasonable, but you wind up changing a hundred places in your source code (... and your callers! hope you have good customer support).

Exceptions are exceptional, they should not be used to return indications of failure. Exceptions should be used when it's lights-out important that something Really Bad be handled or the program will be killed. Most C++ systems I've worked with catch an exception near the top, do some diagnostics, and either tell the use "wups" or attempt to restart.

A "FileNotFound" exception from an open() function is just colossally stupid. I don't have a nicer way of saying that. If I saw code with this pattern in a project that I was on, I would remove it and make sure it didn't happen again.

A "FileNotFound" exception from an open() function is just colossally stupid.

Yes it is, and my example wasn't about such functionality anyway

If you don't close the book and reflect, "I am never writing this shit," Herb didn't do his job.

The same Herb who said Prefer to Use Exceptions to Report Errors? (which makes no sense btw, just like saying to never ever use them)

Depending on another module's authors to get their C++ exception handling right is the road to madness.

Wait, so we should just forget about STL/boost/.. and reinvent all wheels? Or maybe just all code - depending on another module's authors to get their error code handling right, as in not ignoring all of them, is also madness.

You add a new exception that seems reasonable, but you wind up changing a hundred places in your source code (... and your callers! hope you have good customer support).

That is just not how proper code using exceptions gets written, ever. Anyone can make up similar horror stories for error codes (you are checking if HR is FILE_NOT_FOUND or FILE_NOT_ACCESSIBLE but suddenly the author changed the name into FOOFILE_NOT_FOUND and now you wind up changing a hundred places in your source code) Callers know what exceptions they can handle (if they don't the code is crap anyway) and handle those. If a caller wants to provide a strong guarantee the caller catches everything and nothing is forgotten. Everything else is truly exceptional and bubbles up, eventually to the top like all most C++ systems you saw.

"you are checking if HR is FILE_NOT_FOUND or FILE_NOT_ACCESSIBLE but suddenly the author changed the name into FOOFILE_NOT_FOUND and now you wind up changing a hundred places in your source code"

Not weighing in on the broader discussion (in this comment), but more languages support exhaustiveness checking of case statements than of exceptions.

This article caused me to lose my faith in exceptions : http://ptgmedia.pearsoncmg.com/images/020163371x/supplements...
That article is extremely outdated and by today standards quite ill-informed. Many of its arguments could be equally made for error codes.

Try some of these instead:

    http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2012-Andrei-Alexandrescu-Systematic-Error-Handling-in-C
    www.boost.org/community/exception_safety.html
    https://github.com/Quiark/CppExceptDetails
    http://www.boost.org/community/error_handling.html
Regarding what he calls "soft errors" (a better name would be environmental errors, I think), it's unfortunate that option 2 (error handlers) is so often neglected by programming language designers (for example Code Complete doesn't mention option 2 at all).

In my view, just like option 4 (exceptions) is more general than option 3 (return values), also option 2 is more general than option 4. The biggest advantage of 2 compared to 4 is that recovery is much easier. However, more generality also unfortunately means more complexity (both for programmer and runtime). That's why many people here prefer 3 to 4 or 2, and neither one of them is really better than the other.

Common Lisp is a good example of language that has good support for all three. It can return multiple values which facilitates option 3, it has usual exceptions as option 4, and most importantly, it also has signals and restarts that serve as option 2.

I would classify what he calls hard errors into two categories. One is inconsistent input and the other internal (logic) error. This depends on level of view, if you are looking only at one module or a bigger whole (inconsistent input into one module from the other can be considered internal error in the whole).

Now, I don't think inconsistent input should be dealt with asserts. If it's worth checking the inconsistent input at the module boundary, do it and return an error (in any of the three ways mentioned above). The caller should decide whether or not this is a logic error (it may also be bad input from the user), and how to handle the failure.

While I agree with the statement that on logic errors one should generally fail hard, sometimes it's not desirable, for instance in server you may want to just restart the wrong thread instead of shutting down the whole server.

Finally, I think asserts are very much underrated in the current development practices, compared to tests. I wish the effort spent on testing frameworks would be spent on frameworks that would let you add lots of asserts into the code and turn them on/off as needed (for example based on required tradeoff between reliability and performance).

It's a bit sad (and an indictment to "modern" languages) that option 5 "Conditions" (resumable/non-unwinding exceptions) is not mentioned.
How is that different to option 2?
It has nothing in common with 2. It's an extension of 4 where the eventual (dynamically scoped) handler is executed on the non-unwound stack and may unwind the stack (same as an exception), perform an action on the non-unwound system (e.g. resume, resume with a value provided, resume with a restart specified, repeat, etc…) or dynamically opt to resume looking up handlers.
I have to disagree. Historically, conditions and restarts came from Multics and PL/I. There is also similar thing in IBM z/OS, descendant of MVS (ESTAE recovery routines). When ESTAE is invoked, you can decide where you want to recover (since it's assembler you may unwind stack if you have one), or you can "percolate", which means to pass processing on the next error handler. Unix signals is probably another descendant. So in the 1960s, recovery by error handler (the option 2) was very common thing. It only got a bit forgotten later due to them not being properly supported in C, so it didn't get into C++ and Java due to cultural disconnect.
I find anything but a Result<T, E> ADT to be a poor solution. Errors shouldn't have language-level support (exceptions), and error codes are just a poor implementation of a result ADT - they cannot be enforced by the type system.

Result is a monad, so it doesn't require an alternative code path. It's the cleanest and safest.

"Exceptions" should always be for fatal, irrecoverable errors (array index out of bounds). I'm okay with having them, but I don't think languages should support "catch".

How about using an algebraic effect system? That's sort of a general purpose alternative to monads that also doesn't require an extra code path, and adds a bit of theoretical niceness. For example, if your code can throw two different exceptions, composing two monads is order-sensitive (because the monad interface is in some sense too general and forgets too much), while algebraic effects always commute with each other.
True. I could rephrase as "the possibility of errors is just another type, and is best treated as such"?
(comment deleted)
Some interesting discussion here.

First of all it seems that some people are still having a bit muddled vision about what is an exceptional error/condition. Of course you will need to first define what constitutes an exceptional situation in your application and its domain. The blog post used a file opening as an example. That example implied that in that software, in that particular context we have a requirement that our file must be opened. If for some reason it cannot be opened we have grounds for an exceptional condition. When we have this condition we then choose some particular method/means for how to indicate the situation and how to deal with it.

We can also demonstrate this the otherway around. Someone mentioned as an example something like a (web)server. Obviously in that application's domain not being able to open and serve some file is just a part of the normal flow. Clients may make request to resources that do not exist. So in that example that would not be an exceptional circustamce but the "FILE NOT FOUND" condition is just part of the normal conditional flow of the program.

However in that same server we might be for example reading the initial configuration file when the program is started and if we fail to open the file then we might have grounds for an exceptional situation.

So to sum this up, what constitutes an exception situtation is all up to the domain of the program. The blog post discussed methods for dealing with that situation. This is an important point to realize.

Finally a note on assert, yes there are cases when you can't hard fail and core dump on assert as much as you would like to. For example when implementing API's that define error return values for "hard failure" conditions. Whether this is sensible practice and makes the software world better or worse is a matter for another discussion.

No discussion of errors and exceptions could be complete without including Erlang (/OTP)! How does its philosophy of fail fast and rely on your supervisor to restart fit into 1-4? I've been dabbling in Elixir lately, and I really like how easy it makes writing the "happy path", while still robustly handling errors.