80 comments

[ 2.8 ms ] story [ 108 ms ] thread

    only the language runtime and operating system
    would be allowed to raise exceptions
So you still need all the exception machinery, but "normal" code can't use it? Seems like a terrible situation.

Also, who's in charge of deciding what's "language runtime" and what isn't?

It's not really about who can use it, but rather what it is used for - language runtime can't use it for anything they want. So if a systems-level feature of the language is supported in part by runtime code, that's a place where this may happen, e.g. the Go Runtime throwing a stack overflow.

Of course the counterpoint is that C++ and to some extent Java are designed for allowing user code to create system-level facilities, so we're back to the land of "please don't misuse exceptions."

This is how it's done in Rust. panic macro unwinds stack for tracebacks, there's std::panic::catch_unwind, but panic can be compiled to be aborts to do away with stack unwinding, error handling is meant to be done with Result<T, E> type, whereas panic is meant for unrecoverable errors
This is not an apples-to-apples comparison. In Rust, app code can panic, even though it’s not idiomatic. That does not meet the standard prescribed by the quote from the article.
> who's in charge of deciding what's "language runtime" and what isn't?

Maybe the people who define the language? Who implement and package its standard compilers/interpreters? This is not confusing outside the world of empty rhetoric.

throw is being used as a replacement for goto.
All control flow are jumps underneath it all. The only important question about a particular control flow syntax element is: does it allow a human to reason about a particular code scenario more easily.

Most of the time, I think exceptions help more than hurt. Like many things in language design, it is a trade off, in this case of concision vs simplicity.

I've always thought of Java as being extra verbose, in part because there's try catch blocks everywhere.
I've thought of it as being extra tedious.
Wherever there is a try/catch block in Java, you would have at least a few if err return/goto cleanup in C/Go/other error-code languages.

This is especially true when you have a function which performs multiple operations that return the same kind of error (e.g. multiple HTTP calls, or multiple file-based operations). In general, in Java you need a single try/catch block for all of these (which may well be in another function), but you need an if err return for each one in C.

Rust has a more interesting approach, where the sue of Macros gets rid of the verbose if-based pattern, but the code also avoids non-local return. I haven't written Rust yet, but it seems like a very interesting approach.

A lot of try blocks are only there because stdlib declared a lot of interface methods that forbid checked exceptions, requiring everyone to smuggle them out. This nonsense just goes away in Scala and Kotlin.
> All control flow are jumps underneath it all.

Yes, all control flow is ultimately jumps. That's exactly why structured programming was created, to constrain the patterns of those jumped. And it worked pretty well for decades, with if/else and for/while and function composition accounting for 99% of control flow. Goto breaks that pattern, as do exceptions. That's why both should be used sparingly and carefully.

> I think exceptions help more than hurt.

I think the exact opposite. A lot depends on what you're used to. What isn't a matter of taste/opinion is that exceptions lead to a greater separation between where an error is first detected/indicated and where it's handled, compared to error-code returns. There are times when that's appropriate - e.g. code that's just going to retry an entire high-level operation regardless of what error occurred or where. However, I believe that most of the time that's both less appropriate/efficient/debuggable than handling the error closer to its source.

Which by the way was bullied away for no reason. goto has it's uses, but nobody would dare to use it even when it's appropriate
Most of this religion is plaguing young minds when they cannot stand an objective point before authoritative statements (or any statements, as anyone can drive a smartish blog today with a great confidence). I remember how I fought for some of software religion myself and how it took time to get rid of at least worst parts of it.

What is pure evil is articles like this one, claiming something in a pure vacuum instead of weighting a technique in various exact situations and analyzing the outcomes.

Worse, the very same people who sneer at "goto" even when used in a very controlled fashion (e.g. the cleanup-cascade pattern common in kernel code) will turn around and sprinkle their code with a construct that's basically a non-local goto with an unknown (at compile time) destination. They complain about "spaghetti code" and then create code like a toddler's attempt at macrame. Defending exceptions in non-exceptional cases is usually an exercise in hypocrisy.
I disagree. Throw keyword is the greatest invention, and makes programming so much better. Error codes are the evil. Exceptions allow to separate business logic from error handing in a most effective manner. Author likely have not ever worked on large codebases where the error handling was messed up beyond any repair due to having to pass error codes every friggin where
The opposite to exceptions isn't (necessarily) error codes. Golang errors are a recent example.
How does that work? I haven’t seen the recent changes so I can’t say for sure, but AFAIK Goland and Rust error handling mostly amounts to line noise trying to recreate the control flow of exceptions (i.e. bubbling any errors up the call stack) (although Rust tries to make it more palatable using macros).
Then don't try to recreate the control flow of exceptions. Handle things ASAP; only propagate an `Err` if the function returning a `Result<_>` logically has an error condition. Don't be afraid to panic, unless user data is on the line, in which case register an unwind handler and tiny backup facility if you don't trust your programming ability (hint: nobody should trust their programming ability that much unless they've rigorously (mathematically) proven their code's soundness).

It's a different way of programming, but a more explicit one, and I don't think implicit Bad Thing™ should be easy in a language.

(By the way, Rust has a single-character operator to emulate exception pass-through.)

> Don't be afraid to panic, unless user data is on the line, in which case register an unwind handler

Sounds like exceptions.

Secondary return values. It works by being explicit about code paths and error contexts. You lose some convenience and should design simpler, more straight-forward code structure.
Exceptions make f(g(), h()) straightforward. All the alternatives are much harder to read.
Golang level is a tad below functional style or business rules.
It's only straightforward if you omit the error handlers (which is misleading) or don't handle errors anywhere (which is sloppy). Maybe it works for some kinds of programming, but for any kind of systems code it's a pretty strong smell.
The big difference is that in Rust control flow is still explicit. All functions returning errors must declare so with Result, which in turns makes it impossible to forget to handle it on the call site.

This forces you to think: what should I do with this Result error code? Should I propagate it or handle it? This thinking is good and produces good design and good error handling.

For beginners it is very useful, that, for instance, any function doing I/O does return a Result: it forces you to think how your system should behave in case of I/O error.

In C++ or Python, exception control flow is implicit. All functions could theoretically partecipate in an error chain and you have no idea what, if and why a function will raise an exception. Functions doing I/O are completely indistinguishable from the purest functions, and only experience tells you where and if errors should be handled. The consequence is that it’s very hard to find mature code based with sensible error handling.

I should notice that Go is basically the same here: the main difference is that it’s more verbose in error propagation, and requires a configured environment (linter errcheck) to detect when functions only returning err are called without error handling/propagation. Rust is superior here, but Go has the same basic architecture, just less evolved.

You would think just being explicit would produce good design but what actually happened in my experience is that errors are handled by ignorance, as they get hard to pass through. Exactly the same problem as throws clause in Java.

Forcing a global (on thread or coroutine level) handler is better, as actually doing that will mean you have to structure the application in a particular way to handle anything, and that structure makes error handling actually easier.

Just make it fail to compile if the correct handler is not being installed and ensure every error is identifiable.

This is how hardware works anyway internally - interrupt vectors. Having a library to help with correct return and handling of error state is enough. Allowing return only to place that caused the error is saner than what C++ and Java do. There would be no temptation to pass the buck and you would have to model error state to be fully visible, enforcing it to be debuggable and part of explicit API.

You could consider it syntactic salt much like Rust's memory management is.

Golang errors are fancy error codes. They have almost all the same problems, except that they at least don't rely on carefully curating error numbers.

They still can't tell you the call stack, you still need to propagate them manually, they are still very easy to accidentally ignore (especially on functions which don't return anything else), they still add huge amounts of boilerplate, making it hard to read the code and distinguish <happy-case logic> from <regular error-bubbling logic> from <the places where errors are actually handled>.

golang errors are very badly designed. If you want better designed errors-as-return values, check out Rust or Zig.
I would say that the beauty of error codes is precisely that it is an evil that is easily visible, whereas exceptions lurk in the shadows.

Error codes are like someone suggesting "lets blow this thing up" and you can either follow their advice or ignore it.

Exceptions are more like someone throwing you an unlocked grenade. Unless you expect them to be that irresponsible and unless you actively react to their behavior, things explode.

I once had to debug an issue with a Java app that did not throw exceptions anywhere, but it did use the Hibernate ERM framework and somewhere deep inside Hibernate there was a bytecode patching library used which didn't like our custom build of the VM and produce a class-less unchecked exception which would crash the app.

And in my experience, the people that do bad error handling with return codes are also the same people that catch an Exception and rethrow it wrapped in a RuntimeException to remove all that try/catch bloat. It's laziness in either case.

(comment deleted)
I do not think that these 2 extremes are the only options. You can look at Erlang's error handling for an example that both simple and easy to use, yet provides the right amount of information. You do not need to pass error codes either. I usually have a structure that is not too deep and at the bottom there are more specific errors that can be logged very close to the codebase where the error happened and propagate only error or ok to the caller. Even with large codebases this is pretty manageable.
And the performance argument is moot. Yeah don’t use exceptions in a tight loop, but as its very name suggests, this is to handle a hopefully rare branch of the code.
I strongly believe that the Go approach is the correct way to go. Every function that can fail should return an error code and then the programmer invoking said function is expected to handle the error code. It also used to be that way back in the old C / C++ days before SEH made it messy like Java.

And the beauty of using statically typed languages with error code return values is that one can use automated tools to find all the places where error codes are being ignored. So then random error conditions sprinkled throughout business logic go back from "this is how Java is done" to "this is a code smell".

The old C way was returning an error indicator and providing the error code in the global variable errno(), which has ... issues.
A few more steps and you'd have reinvented functional programming.
>Every function that can fail should return an error code

Every function that can fail, that can take invalid arguments, or can be called at invalid times. Which widens the set to basically every function. Old APIs had a convention to always return a status directly and results in out-params, except for pure mathematical operations (in which errors were mostly ignored, e.g. in a computer, a+b may fail as much as fopen(), but who cares).

Observing this fact, one can free themselves from:

  if ((status=operation(args, &vague_error_descriptor))
    return status;
and handle the specific error event where it seems reasonable in the way that seems reasonable.

Unwinding call stack by hand doesn’t automagically handle errors. At main() or at the other high-level call site you still have no idea what to do with an unexpected error except logging/presenting it and exiting (or continuing in hope invariants were not broken).

>one can use automated tools to find all the places where error codes are being ignored

The same is true for exceptions - every milestone like db.open(), file.save() and process.spawn() should be try-catched. And in both cases you don’t know which errors “codes” are still unhandled.

You are describing panic() which may happen due to programmer error, but not from a+b or any arbitrary event. It follows a short spec and stdlib, and may even be recoverable. Your if-example is old style and not allowed in golang. Your milestones escalates any small deviation into potential crash-handling territory (good example).
I do not now how many hours was wasted on trying to debug Java applications that did not have this principle and produced 10000s of lines of stack traces in production with cryptic exceptions.
Which principle would that be? With Java, even if you do a bad job of error handling, you usually still get at least a stack trace that you can investigate based on. If you do a bad job with error codes, you can easily be left with no way of investigating a problem other than trying to reproduce it.
Yes?

Let me tell you just one story about this. I used to work as a systems engineer for one company where we had Hadoop as the data warehouse solution. One day my co-worker decided that he is going to get into DevOps and his first thing will be to touch DNS settings. He also thought that the best course of action is to push his changes without a review. His code was deployed to staging.

I was called into to investigate an outage on staging later on. Needless to say I was unaware of the changes but this is not the point. Hadoop datanodes were failing with the following:

"Cannot connect to NameNode: host.name.tld port x"

I was was trying to investigate why. Unfortunately I did some quick testing as root and I could connect to the host and port combo. I went through all the changes in Hadoop configuration but there was no change that could cause this. Finally (several hours later) I wrote a simple Ruby code that did the exact same thing and I run it as hdfs user. I run the code and it failed with the following:

"Cannot open /etc/resolv.conf"

As you can see the Java codebase was failing with the same reason but nobody bothered to check for this problem in the library so the exception that you cannot open a file resulted in a different exception. It turns out my co-worker was very eager and security aware and he changed the resolv.conf permission from 444 to 400. I also made the mistake to investigate this as root first and not going through the unrelated changes. If Hadoop's Java was decent enough it would have printed the exact error message instead of something that is related but not exactly the error you are facing. This is what I push for regardless of the language, catch the error exactly where it happens and log it there and I think this is what the parent was referring to as well.

This is a good example,and the error returned by the Java DNS library is obviously wrong. However, I don't think that simply returning the file IO exception would have been the right solution. The best solution would have been for the DNS resolution error to have carried the FileIO error as a Cause. That way, code that could recover from a DNS resolution error could handle the DNS resolution problem, and code that couldn't would still log the actual root cause.

The Java networking libraries seem to be particularly poorly written for DNS error handling. I encountered an error where I was opening a socket to connect to a hostname, with an explicit timeout of a few seconds (it was an external service, but the application was supposed to work offline as well). To my surprise, the call would block for a few minutes on a customer system. It turned out that the call was first resolving the hostname, and that the timeout value I supplied was not applied to the DNS resolution, which was using a system-wide default timeout.

One other point about your example: I think that one thing missing from all language runtimes that I have seen is a proper logging mechanism that is truly available language-wide. Of course there is always stdout/stderr, but these are usually far too limited and it is nkt considered good form to log to them from libraries. Even if you did, you would obviously not respect the application logging format in regards to stuff like time format, timezone etc. In contrast, a standardized configurable logging mechanism would help so much with 3rd party library debugging.

Stack traces are a bad way to make sure your program has sound logic flow, though may be convenient when lacking tracing and validations.
Except if your framework uses bytecode patching, e.g. Hibernate ERM, in which case the Stack trace shows you classes that do not exist in source code form.
You may believe that of course, but the reality is that we have no proof either way. I personally believe the opposite, that Go-style explicit manual error handling obscures both the business logic and the error handling logic, and makes it much harder to read code. Unfortunately, we don't have any studies that could shine a light either way, and it is very likely that if we did, we would see something similar to the typical static-vs-dynamic typing study : no real effect on defect rate; no way to accurately measure productivity.

> And the beauty of using statically typed languages with error code return values is that one can use automated tools to find all the places where error codes are being ignored.

Even better, the beauty of garbage-collected languages with exceptions support is that errors can't be ignored at all! And if you don't have garbage collection and need to manually cleanup every resource, both approaches get a lot more difficult, and it's much harder to tell which ends up being superior.

> one can use automated tools to find all the places where error codes are being ignored

What I want is an automated tool to display the 1/3 of code doing useful work, and hide the 2/3 useless noise that should have been generated. We shouldn't make human beings read exception bubbling boilerplate for the same reason we don't make them read stack frame setup boilerplate.

IntelliJ is doing that nicely for the anonymous classes needed for Java callbacks.
We've moved past it now with having errors just be part of the type and signature so you can use language tools to deal with them like any other type instead of having special syntax. I like that. It allows you to express certain concepts more easily. For instance, "in case of failure, use blank" looks nicer in Rust than in Java, and so you're more likely to use it. Java is also sort of moving this way because checked-exception methods don't play well with Streams syntactically.
> The Year Everything Crashed

Which year?

> If you were using computers in the years that followed you remember the time well even if you didn’t know the reason; a lot of software that had been stable and solid before started crashing and misbehaving.

I think I missed this.

Well lp0 would catch fire, but it wasn't as bad as using throw!
I love the way Elixir/Erlang handles this, personally.

If the error is expected/should be handled, just add the error to your pattern match and handle it right there (or bubble it up, if appropriate).

Truly exceptional situations where something "should never happen" won't be pattern matched against, BEAM will crash the process, and your supervisor will get you back to a good state.

Feels so much cleaner than either exceptions everywhere or easy-to-ignore error values.

Didn’t Java require all exceptions be declared in the method signature? (Haven’t used it in years.)

I think that’s the best solution. You declare what error conditions could arise, but not using magic numbers. The caller can then decide whether to handle them or pass them to its caller etc.

I agree that unexpected exceptions (like it’s possible in C# and possibly others, you can just throw whatever whenever) are far from optimal.

Initially yes, but this is somewhat unpopular these days as it means you have to daisy chain a new exception all the way up the stack if you can't handle it locally.

Most have switched to runtime exceptions, which are more flexible in that regard.

No. Only a certain subset, known as “checked exceptions” (as opposed to “runtime” exceptions), need to be in the signature.

In what may come as a surprise, checked exceptions are actually frowned upon in modern java (e.g. new stdlib apis tend to avoid them). They do not compose very well with the rest of the type system (e.g. you cannot write a ”map” function that is polymorphic over checked exception types)—or at the very least, do not compose as well as some other patterns, like Optionals

Hm okay. If it’s not “compatible” with streams, higher-order functions and whatnot I can see why it’s not popular. A pity, really.
Checked exceptions sound good on paper. In practice I think I re-threw every checked exception as unchecked.

In retrospect I feel like 'handling' exceptions is a myth. If you knew how to handle them, they wouldn't be exceptional.

Checked exception interact poorly with higher-order functions, but are otherwise pretty nice in my experience. If Java had a more powerful type system to allow it to abstract over exception type, it may have been a much more liked feature.

Regarding handling, in my experience the vast majority of code has no way of handling an error (whether we're talking about exceptions or error-codes or Result<T, E>) except for logging and returning it up the call stack, or immediately aborting soemtimes.

But there are places in the program where there rely is enough information to handle an error from a whole sub-module, usually by retyping with other params, by asking the user for advice, or by simply telling the user that something they did was wrong. Getting the right amount of error information to this place is the problem that exceptions solve the best in my opinion.

When 'pair programming' was chucked in as an example of one of the worst things that happened to programming I lost interest.

Back up your assertions with facts. If you don't have facts use good truthful examples.

Too much opinion and conjecture isn't useful.

This really reads like an article by someone who likes to think they know better than everyone else, while liking things that the industry has tried and moved away from.

I would much prefer a similar sounding article from someone who champions some weird technology, like people APL people who advocate for extreme conciseness of code, or Forth people who advocate for extreme integration of solutions. I don't think they're right, but at least they would be novel and would advocate for something we haven't moved away from as an industry for a reason.

Rejecting the entire main argument because of a single aside - a matter of style more than substance - is effectively argumentum ad hominem. Yes, the author comes across as a grumpy old "get off my lawn" type (like me in fact). No, that doesn't make any of their arguments or anecdotes about throw invalid. Try addressing the issue, not the person.
While you may be able to sympathise with the author's style of writing, I think my point still stands.
Your point seems to be that it's OK to reject an entire argument because of one irrelevant aside. I guess that "stands" as a personal belief, but it does not stand as an argument or refutation of one.
The keyword, and exceptions, are orthogonal concepts that this post is conflating. The real issue this post has is with exceptions (and in particular user-defined exceptions, though I'm not really sure why this matters because you can always (ab)use system exceptions if you have a way to reliably trigger them). You can still use the `throw` keyword with other error-handling models, such as with Swift's Errors.
I worked at a telecom company. They had a program written in java for generating the initial customer configuration.

Among other stuff it used exceptions as control flow and it made me make extraordinarily high estimations for seemingly simple changes.

Anyway, I don't think I agree with the article - it's again the matter of the knife and how you use it.

I feel like with errors we're always in a state of sin because the way errors create a combinational explosion of code paths.

The only thing I have to say is I have a distaste for throwing exceptions across API boundaries.

> it's again the matter of the knife and how you use it.

The author makes this point repeatedly: exceptions would be great if all programmers were above average, but (of course) they're not. In practice the knife is juggled and thrown (heh) and generally mishandled so much that the floor is covered in blood. This gets us into pg's whole "blub" design-for-geniuses vs. design-for-idiots discussion, with is basically dueling strawmen, so I'll bow out there.

Could some sort of hybrid approach be an improvement? What if we required every function to explicitly declare the types of exceptions it might throw? The compiler could enforce it and make it transitive to higher level functions.
Except for the transitive part, this is what Java does. But it's doesn't address the complaints of the article, which is bothered by the non - local control flow and the large cost of the exception machinery in C++, and by the poor interaction between manual memory management and exceptions.

Note: I think the article is vastly exaggerated, and completely wrong, at least when looking at managed memory languages.

That was the basic premise of checked exceptions in Java.

It's not really related to the type system or anything. The root of the problem is Java compiles each class as an independent module, so any method has to deal with all its exceptions, even if only naming them, or it can't compile. Thus checked exceptions meant you either got huge signatures, or you wound up wrapping them all into other exceptions, which then made them much harder to inspect.

And unchecked exceptions made the problem worse. Java uses single-inheritance and put unchecked exceptions on their own branch. This meant you wound up with, for example, IOException and UncheckedIOException with no close common ancestor, so you had to wrap one in the other. So now, in addition to complicated signatures, you are doing more wrapping.

I think what might work would be to ditch the checked / unchecked idea, mostly. (You still want to treat errors like division by zero or invalid array indexing as unchecked.)

What you distinguish instead is whether methods are "safe" or "unsafe". An unsafe method can still catch some exceptions, it just doesn't promise to catch all of them.

The safe method must catch everything. For example, your main method and Thread.run would have to be safe.

Throw in Java is very inexpensive. Expensive part is creating new instance of exception, which figures out stack trace and creates bunch of immutable strings.

There is a technique of throwing existing static instance of exception, which generates no garbage and two orders of magnitude faster than traditional java approach:

https://github.com/questdb/questdb/blob/7560895ee400c82b0e5e...

The only downside is that stack trace is effectively defunct.

I agree that `throw` is easy to misuse; but I think this is a symptom of something else, namely in architectures where input even handler code is spread all around or, worse yet, dynamic. This is really common in many languages, and perhaps my greatest pet peeve.

However, throw is a wonderful facility if your architecture has one defined entry point for inputs of all kinds. It means your outermost loop can have a try block, and literally everything else can feel free to throw in the safety and comfort of knowing the entire program won't fall down.

goes on to explain misuse.. jk sort of.

What when allocation fails or disk full etc?

> What when allocation fails or disk full etc?

It is a common misconception that such errors can be easily handled. Most of software is too complex to be able to do something reliable with hard OS issues. Stack unwiding with exceptions is the most quick way to disaster with ENOMEM as root cause.

Error propagation is must have for any project. You always need to handle some errors in parent context because local knowledge can be not enough. And you can choose between manual passing of error codes (c, go, mundane and boiler-platy), algebraic errors (rust, zig, optional sugar can make it convenient and look like SEH) and SEH. Yes it is additional condition flow, but you have to introduce it in some way.
Publishing on hackernoon is a mistake. The site sucks and also disables the Reader View that would make it tolerable.
No, article corresponds to unicorn-rainbow-coin-rebel style of most hackernoon content very well.
Can someone point me to a code example (preferably TypeScript or JS) to clarify what the author proposes as an alternative to ‘throw’? Maybe it’s Stockholm Syndrome, but I’m struggling to picture how returning an error code (and checking it with an if-statement after every function call) would make code easier for a human to analyse. Seems like it would just make it a lot noisier and less expressive. But I’m intrigued.
The problem comes when programmers muddle and get confused about different possible "failure/error" conditions in a program.

A) Bugs, i.e. errors made by the programmers of the software B) Expected logical conditions that only appear as "errors" to the users C) Unexpected failures such as resource allocation failures

Let's have a look at these one by one.

Case A, i.e. bugs written by programmer, i.e. the program is about to violate it's own constraints/logic/pre/postconditions/invariants. Examples are many, but for example off by one, accessing nullptr/null object, array out of bounds etc. Really the best way to deal with these is to abort/panic/dump core with a loud bang and produce a core dump and a stack trace to simplify debugging.

Case B, i.e. conditions that the application should expect to encounter and be prepared to deal with. For example your TCP socket connection times out, DNS query fails, HTTP query response returns 404. In any such case when you need to write some logic such as "retry to open the sockect connection" or "tell the user that the resource was not foudn" or try to resolve DNS query again later the best method is to use some kind of error enum/number/code kind of system. And to recap the "errors" in this category are not errors for the software but only errors for the user. I.e. for the software these are just logical conditions.

Finally Case C), errors that are neither A or B. Typically these are just some very unexpected conditions such as resource allocation failures. Your program failed to allocate a socket or pipe or a mutex or whatever. Should not happen but might happen once in a blue moon under heavy system load or unexpected conditions. These are the things you don't want to conflate with your logical "error condition" path from case B because't that won't scale and propagation of errors up the stack becomes very tedious. These are best suited for exceptions.

Edit: Note that this isn't a static state but what is considered a bug or a logical error can change across modules depending on the software project organization. A module written by another team migth consider illegal arguments as "Logical error conditionds" and return error codes, whileas a similar module written and used within a single team might just consider this a bug and dump core.

Agreed. Exceptions should be, well, exceptional. Not normal parts of control flow, or commonly occurring kinds of errors. Assertion failures, or other things that would otherwise send a signal and terminate the process - seg fault, divide by zero, etc. Maybe something like a socket being closed unexpectedly. That's kind of the boundary where it's worth debating. Anything more common/expected than that should be represented as an error code and not completely redirect the program's control flow.
> Dealing with them in a more elegant way than inelegantly crashing goes all the way back to 1962 in a near-forgotten language called LISP.

Even as someone who isn't a Lisp programmer, I find the idea that Lisp is a "near forgotten language" to be a bit of a stretch.

> Structured Exception Handling (SEH) was born.

This, including the abbreviation, is the name of a feature of the Windows API.

The author is confused between exception handling in programming languages, and exception handling in the MS Windows operating system.

https://docs.microsoft.com/en-us/windows/win32/debug/about-s...

https://en.wikipedia.org/wiki/Microsoft-specific_exception_h...

Note that in Microsoft C++, this uses special __try, __except and __finally keywords, which use underscores because they are extensions, separate from the ISO C++ try and catch.

SEH can catch events like access violations. It's more like Common Lisp exception handling in that you can catch errors without any stack unwinding taking place, so that fixing up a fault and re-starting an instruction is possible (like catching a SIGSEGV or SIGBUS on POSIX).