The Throw Keyword Was a Mistake
hackernoon.com
hackernoon.com
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>.
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.)
Sounds like exceptions.
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.
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.
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.
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?
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.
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."
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.
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.
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.
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".
> 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.
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.
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.
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.
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.
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.
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.
The only thing I have to say is I have a distaste for throwing exceptions across API boundaries.
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).
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.
Most have switched to runtime exceptions, which are more flexible in that regard.
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
In retrospect I feel like 'handling' exceptions is a myth. If you knew how to handle them, they wouldn't be exceptional.
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.
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.
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.
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.
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.
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.
Note: I think the article is vastly exaggerated, and completely wrong, at least when looking at managed memory languages.
Back up your assertions with facts. If you don't have facts use good truthful examples.
Too much opinion and conjecture isn't useful.
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.
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.
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.
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.
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.