The Scourge of Error Handling
CowboyRobot writes "Dr. Dobb's has an editorial on the problem of using return values and exceptions to handle errors. Quoting: 'But return values, even in the refined form found in Go, have a drawback that we've become so used to we tend to see past it: Code is cluttered with error-checking routines. Exceptions here provide greater readability: Within a single try block, I can see the various steps clearly, and skip over the various exception remedies in the catch statements. The error-handling clutter is in part moved to the end of the code thread. But even in exception-based languages there is still a lot of code that tests returned values to determine whether to carry on or go down some error-handling path. In this regard, I have long felt that language designers have been remarkably unimaginative. How can it be that after 60+ years of language development, errors are handled by only two comparatively verbose and crude options, return values or exceptions? I've long felt we needed a third option.'"
Ignoring the error completely, data integrity or planned functioning be damned.
I think MS already tried the blue screen.
BASIC has had it all along!
What a fool believes, he sees, no wise man has the power to reason away.
Exceptions should NOT be used for 'normal' errors. They should be used for events that are, well, exceptional. A healthy program should NEVER raise an exception, but may deal with a lot of error conditions.
While I have seen good error handing schemes in many languages, so far, I haven't seen anything as good as C++ exceptions combined with RAII. Exceptions alone aren't that great, but if you combine it with the way constructors / destructors work and compose in C++, it ends up working really well. A lot of languages with exceptions lack RAII. Java and C# have exceptions but don't have destructors (the language equivalent is much less useful than C++) much less ones that compose.
The only real problem is that lots of C++ code rely on return codes, no error handling at all, or poor use of exceptions and resource management. There are lots of C++ programmers who stumble on error handling code and haven't learned how to take advantage of the tools the language provides. Of course error handing logic can be quite hard, even if the language helps out a lot.
STM is also a great way of doing error handling. Transactions (like used in databases) make error conditions much easier. But they cannot be limited to databases; transactions in the file system (Microsoft has this with NTFS) and transactions in memory data structures (STM) are very valuable.
Visual Basic had:
On Error Resume Next
I last typed that when I was about 13...
The documentation shows a couple of valid uses for it.
Normally exceptions should be used in exceptional cases, not in normal control flow. Exceptions are usually quite expensive, especially in C++ compared to just returning an error code. Language APIs should be fast, but also convinient so they had to made a trade-off.
It is not clutter. It is necessary. Trash cans in the home might be considered clutter too I suppose. Some people artfully conceal them within cabinets and such, but in whatever form, they are both necessary and either take up space or get in the way or both.
It is the reality we live in. If you want to code in a language that doesn't require error handling, you might look to one of those languages we use to teach 5 year olds how to program in.
Good code does everything needed to manage and filter input, process data accurately and deliver the output faithfully and ensuring that it was delivered well. All of this requires error checking along the way. If you leave it to the language or the OS to handle errors, your running code looks unprofessional and is likely to abort and close for unknown causes.
I think the short of this is that if anyone sees error checking as clutter or some sort of needless burden, they need to not code and to do something else... or just grow up.
How can it be that after 60+ years of language development, errors are handled by only two comparatively verbose and crude options, return values or exceptions?
Of course, it could be that this just means that your own language horizons are too narrow. Prolog and icon come to mind.
Lacking <sarcasm> tags,
Exceptions here provide greater readability
Nah, they don't.
How can it be that after 60+ years of language development, errors are handled by only two comparatively verbose and crude options, return values or exceptions? I've long felt we needed a third option.
Maybe - and admittedly this is just a guess from my fairly ignorant viewpoint - it's a very hard problem. How can it be that after 100+ years of industrial development, we're still heavily reliant on internal combustion engines to get us around? Why have we only got people as far as the moon in 60 years of space travel? Why, after x years, have we only achieved y?
Because that's the way it is. Is there some reason we should have the third option by now?
systemd is Roko's Basilisk.
The author commends the use of multiple return values and a side-band error value that must be checked? Gee, multiple return values have been in Lisp forever, and maybe he's not aware of this little thing called "errno"?
Error handling is very, very tedious by nature. There are bajillions of ways that a system can go screwy, and many of these have individualized responses that we want distinguished for it to behave intelligently in response. We expect computers to become "smarter", and that means reacting intelligently to these problematic/unexpected situations. That is a lot of behavioral information to imbue into the system, all hooked into precise locations or ranges for which that response is applicable. That information is hard to compress.
The key to taming exceptions is to use them differently. Any exception that escapes a method means that the method has failed to meet its specification, and therefore you will need to clean up and abort at some level in the call chain. But you don't need to catch at every level (unless your language forces you to), nor should you need to do anything that relies on the "meaning" of the exception. Instead, you take a local action: close a file, roll back the database, prompt the user to save or abandon, etc, and either re-throw or not according to whether you have restored normality. There will only be a few places in your app where this type of cleanup is needed.
If you're not doing it this way, you're using exceptions as a control structure, and that's never going to be clean.
Paid Q&A/Research
This is the real third option.
The so-called "error returns" from things like file opening are telling the program something very important about what's going on. The program's flow must be designed from the beginning to interpret and handle errors. This is in fact be much of what a good program does.
It doesn't matter whether we use exceptions or error codes to signal the errors as long as the program is designed to accurately interpret the errors that do occur. In some sense, exceptions may be easier to implement in today's event-driven interactive interfaces. Regardless, though, the design must not allow errors to be lost.
Was it Cooper in "About Face" who said that an error alert pop-up was essentially an admission of failure on the part of the programmer?
Rick.
errno is just a return code in other clothing.
Python: 'And then suddenly you have a language which says "we're all stuck with whatever the whiniest coder wants".'
Monads are fun for error handling. :)
I donno if they present exactly what the author might consider a third option though, well certainly they can present other options, like with the Either monad, but that's no simpler really.
The Christian religion has been and still is the principal enemy of moral progress in the world. -- Bertrand Russell
Just don't bother checking for errors.
1984 was not supposed to be an instruction manual.
A lot of coders just cover entire routines of code in t/c blocks because they don't really want to handle errors at all.
Ummm, I think that depends. If you enclose a code block with ~some~ exception handling, you obviously know there is a possibility for a problem. Its what you do with that exception that separates the coders from the slackers. Also, when coding an API, there's little more appropriate than using a "throws" clause, it should be up to the caller to deal with raised exceptions as it sees fit.
That said, exceptions are so expensive I tend to favor return codes in speed-sensitive code and turn off exception handling if the compiler allows it.
Python: 'And then suddenly you have a language which says "we're all stuck with whatever the whiniest coder wants".'
Normally exceptions should be used in exceptional cases, not in normal control flow.
People keep saying that, but I've yet to find someone who can defend the position with a logical argument.
Fundamentally, you run some code to do a job. There are two ways it can finish early: either it succeeded, and we did all the work/figured out whatever information we were asked for, or it failed, and maybe we want to report this along with some related information. Either way, there is nothing useful left to do except hand control back to the higher level code that asked for the work to be done, along with the outcome of that work, as efficiently as possible without leaving anything in a mess as a side effect.
Exceptions, as provided in many mainstream programming languages today, could serve either purpose just fine. The semantics work the same way in each case. The performance implications are the same in each case. Aside from the unfortunate name "exception", which we could replace with something like "outcome" or "result" just as easily, and the commentary of certain commentators, whose arguments are rarely more than an appeal to their own authority, there is no difference between the two cases semantically or in terms of the code I want my computer to run.
So, why should exceptions be used only in exceptional cases, apart from dogma or convention? They're just a tool, like variables or functions.
Exceptions are usually quite expensive, especially in C++ compared to just returning an error code.
I'd like to see your profiling results to back up that claim. I've got a few years of working on high performance code that suggests most compilers from the past decade or more use some variant of table-based dispatch to handle exceptions. That means they will not need to manually unwind the stack step-by-step in the case where the exception is thrown/raised; they can just run any necessary clean-up handlers and otherwise skip over everything between throwing the exception and catching it. It also means there will be less error checking code required all the way up the call stack in every other case. In other words, this model runs faster and it does does so whether or not an exception is thrown. The overhead is in the space for storing the jump tables and the compiler's effort to generate them (both of which can be unpleasantly large) but not in the run-time speed.
If you disagree, post your argument. (-1, Overrated) isn't your personal censorship tool for views you don't like.
There are two ways to do error-handling: try{}catch{}, or if{}else{}. That's "using exceptions" and "using return values", under Dobb's naming.
The difference in usage is simple: one handles errors immediately, thus cluttering the code with all the things that could go wrong, while the other separates error-handling out, pushing it to the end of a block (and away from the code that actually generates the error, which can complicate debugging).
I can really think of no other way to do it. You can handle the error where it happens, or handle the error at the end. I tend to look on anyone whining about how hard error-handling is with suspicion - their suggestions (if they even have any) are almost always "the language/compiler/interpreter/processor/operating system should handle errors for me", and there are enough obvious flaws in that logic that I need not point them out.
In other words, you do not know how to use them effectively therefor any use of them must be bad?
T/C blocks are a tool, just like everything else in programming. They can be abused, and they can be perfect for the job. Anyone who tries to claim that some tool is universally bad and has distain for any piece of code using it regardless of its appropriateness does not strike me as a very good programmer, or at minimal a very limited programmer.
You don't write an article like this unless you're actually going to suggest a different solution in it. Otherwise it just comes off as whiny and inexperienced. "Oh, if only we could not do that thing that everyone must do if they want robust code!" Reminds me of beginner CS students who don't want to make an extra header file or prototype functions. We're not doing magic here, and no amount of wishing for magic will make it happen. Work with some magic module (ActiveRecord, maybe) for a while and you'll quickly learn to hate magic, anyway. Discipline is required to write code that will stand the test of time. If I were wishing for something, it'd be that more programmers had the discipline to write good code consistently.
I'm trying to teach myself to set people on fire with my mind... Is it hot in here?
And I must say that as the Editor in Chief he has a very simplistic view of the problem. If I understand, his view is that a global exception added at the compiler level would somehow solve all the problems. He gives the example of calling "open" without worrying about it failing. Of course he doesn't state how to handle the failure when it occurs. For example
open(file1); // ok // failure
open(file2);
What happens to file1 in this case? How is the code cleaned up? There may be a case where you don't want to just close all files in the functions, but just create file2 if the open failed. (for example).
His complaint is that there is too many options available for error handling, and that they lead to cluttered code. As far as I can see the alternative is not enough options available and code not always doing what you want, and having to fight the compiler in order to get what you want.
This is generally seen with asynchronous code, but it could apply anywhere.
Consider: (javascript) XmlHttpRequest has a readystatechange callback. Most javascript libs wrap it up so you pass in two callbacks, one for success and one for failure/error.
e.g.
jQuery.ajax(url, { ... }, ... }
success: function(data, textStatus, jqXHR){
error: function(jqXHR, textStatus, errorThrown)) {
);
No return, no exception, the programmer decides how to handle it.
Do you even lift?
These aren't the 'roids you're looking for.
Problems don't exist in reality, they exist in points of view.
Don't complain about syntax, grammar, or spelling. There is no.hell like input on android.
Quality code will always be "cluttered" with data validation code, result verification, and a host of other details.
The simple fact is that computers are stupid. They have to be explicitly told what to do in every conceivable situation the code could encounter at runtime, or else the code will crash and the user will complain about it being "unusable".
I notice that despite the article author's bitching about the situation, they had not one suggestion as to what to do instead. It's easy to bitch about life, but a lot harder to suck it up and deal.
If exception handling and return-value checking code are "too hard" for someone to understand, they need to get the hell out of the programming industry and leave it to professionals who actually find it fun and challenging to deal with all the details. Not everyone has the mindset of a true programmer.
I do not fail; I succeed at finding out what does not work.
All coding should proceed as if every possible exceptional condition (device not ready, cache fail, controller failure, cat dials 911 on speakerphone) is the primary and intended purpose of the Project. Hash collisions not merely covered as a contingency but pursued with vigor in the main line to the Nth degree, where N indicates the infinitesimal possibility of multiple simultaneous hash collisions that would be the likely result of a vengeful god constructing the universe such as to produce a life of continuous and foul exceptions.
When gathered at the water cooler, coders would discuss triumphs in their particular areas of malfunction, and when they corroborate as a group it is to merge their respective threaded exceptions into a parallel paroxysm of failure, branching with virtual threads and physical coring such that the greatest possible number of malevolent conditions are met and coded for, simultaneously. Proceeding steadily towards the grail of the Grandest Failure.
The Grandest Failure being the stuff of mere legend, yet it is what drives us. It represents that supreme and sublime moment where everything that can go wrong has gone wrong and the very fundament reeks of wrongness.
Buffers are not starved as an exception, they are starved by design! Disk controllers are never ready. Communications packets never arrive in sequence, or so we assume because there are no markers to check, when they do arrive they are garbled beyond repair. Reconstruction occurs as a matter of course! Streams are unsynchronized by nature, incompatible by rote, unresolvable.
Off the corridor in a dusty hallway a small team of pariahs is assembled to perform the dirtiest and most detestable task of all: to handle the exceptions and branches thrown by the main line, conditional branches sketched out briefly (whose existence is known but not mentioned in polite conversation) are pursued in secret. This is necessary work but unrewarding as it leads away from the noble purpose of Grandest Failure, towards useful work. Such stuff as consolidation, transaction handling and data ordering, forgive me for uttering, Chaos be Praised!
For the goal is to produce a System that boldly and efficiently proceeds down the pathways of most numerous and most simultaneous failure, where the actual success of anything triggers the exceptions and is cast off to the side.
If robustness of design becomes human sentiment, it could be said that the System confidently strides forward boldly embracing every error condition and is shocked -- horrified -- every time something goes 'right'. As life's own experience is our guide, it is seldom disappointed.
The output of useful work in such a System the source of great embarrassment and discomfort, a necessary evil.
That is the principle behind the control systems of the Improbability Drive. It is the driving principle of the quantum flux, Brownian motion and wave/particle paradox.
All of this Order and Progress (blaspheme!) is but a side road off a side road ad infinitum. The main path leads to Chaos. Follow that path and revel in it. There is no honor in coding for success, any idiot could do that.
Down deep people know this is the Way. That is why when coders meet in dim conference rooms and the slideshow laptop suddenly projects a Blue Screen of Death for all to see, there is an eruption of thunderous applause, as if one had dropped a tray of food in a crowded cafeteria. Deep down we know failure is the noble path, and success the exception.
<blink>down the rabbit hole</blink>
Exactly. That was my first thought on reading this: "If there's a better way, show us. Come up with a solution. What's stopping you?"
In reality it's not that obvious, or someone would have thought of it already. I would look at engineering practices and see how they handle failure modes. Sometimes it is better to let the thing break as long as you design it to do the least amount of harm when it does.
It's possible to develop defect-free software as long as all factors are under your control. E.g. a program that runs on unreliable hardware can never be made reliable.
"Slow down, Cowboy! It has been 3 years, 7 months and 26 days since you last successfully posted a comment."
When Chuck Norris throws an exception, it is always fatal.
How is the Riemann zeta function like Trump rallies? Both have an endless number of trivial zeros.
Monads monads monads monads monads from Haskell.
Or "workflows" in F#. Related to, I think, "generators" in Scala?
Roughly (and I'm going to make up some C++/Java style syntax here), you write something like this:
workflow someExpressionMaybeAnObject
{
int x = someassignment;
some statement;
someotherstatement;
}
At the end of each line, you check the return value for errors, and use the handlers defined by the object up top, which could short-circuit the rest of evaluation.
These are actually a lot more general than error handling. For example, they generalize Python-style list comprehensions when used in a certain way.
In Haskell-land, there's a lot of interesting math about how they work, but you don't need it for error handling.
Moral: Learn a wider variety of languages!
Surely there are different sorts of errors, which would suggest different approaches for dealing with them?
I guess it's pretty hard/futile to deal with most of these issues at a language level, because the appropriate course of action and channels of communication depend on the system. It strikes me that most of this stuff is something a domain-specific framework or API should be handling.
POSIX signals themselves are a bit of a horror. Like C++ exceptions (as Google correctly points out) they have implications for `other' code, the worst case being code that has not be written to cope with interrupted system calls. Also, signal dispatch has portability problems; signals did not anticipate threads and POSIX was slow and iterative in its promulgation the standard solution, so many subtleties have appeared among implementations.
However, I think you have the right instinct. I personally find myself working in explicitly event driven environments frequently. Node and TCL for example. Here you can not indulge the illusion of absolute control over the fate of the instruction pointer. Any time you `yield' to the runtime you wind up entering your code at some other point as the runtime dispatches events.
Using the event model to cope with errors and exceptions would mean that anything that would traditionally throw an exception or return a error code would instead be a yield point and may generate an error event. You would then provide a handler to receive these events with enough context to cope with the problem.
I've come to the believe the event driven model is a far better model for the actual conditions one assumes when implementing logic. The moment you write main(){...} you are subject to signals that are handled by a collection of default handlers. One day the system becomes non-trivial and you must 'fix' these handlers. Perhaps you have no business writing main(){...} and adopting a naive, linear model in the first place. Instead, you're supposed to implement (the moral equivalent of) a signal handler instead.
Down at the bottom, where CPUs process machine code, hardware interrupts are endemic. The hardware itself imposes the event model. It may be the case that most machine/assembly code still written by humans today are simply event handlers; logic servicing hardware interrupts.
Lurking at the bottom of the gravity well, getting old
Functions either succeed or fail, period. That statement is fact.
Alan Turing and Kurt Gödel might not entirely agree with that statement. (Functions can also not terminate, and it can even be impossible to work out if they are going to terminate or not.)
"Little does he know, but there is no 'I' in 'Idiot'!"
The fundamental problem is that sometimes an error is an error to the calling program, but sometimes it is not.
For example, when you issue: open "$HOME/.myconfig", the inability to find the file does not mean there is an error. Just that the optional config file is not there. But when you try to open the source file for an operation, the open-error really IS an error.
This duality happens at most levels. A library wrapping "open" will have the same problem. Does the caller consider this a fatal error or not?
Similarly, sometimes errors should result in telling the user and then quitting. But for a gui application it's better to show a graphical message and continue, even if the error is more or less "fatal".....
Mod parent up, I'm in complete agreement: "If there's a better way, show us. Come up with a solution. What's stopping you?" And indeed it's not obvious, or we would already have the solution.
I'm not sure I agree that it's possible to develop defect-free software. All hardware is unreliable. Mean time between failure.
Perfect software may be perfect in our minds; but software immediately degrades when implemented as machinery.
Perhaps the original poster is frustrated by the perfection in our minds failing to overcome the limitations of physical reality ... much as we all wish to live forever, even though we know that's not going to happen.
-kgj