Exceptions vs. status returns
Created 15 September 2003, last updated 14 October 2003
Broadly speaking, there are two ways to handle errors as they pass from layer to layer in software: throwing exceptions and returning status codes. Almost everyone agrees that exceptions are the better way to do it, but some people still prefer status returns. This article shows why exceptions are better.
The examples here are in C++, because that is the primary battlefield for this argument. Older languages like C don’t have exceptions as a real possibility, and newer languages like Java, Python, Ruby, and even Perl provide facilities for exceptions, without the cultural heritage that encouraged status returns in the first place.
Clean code
Exceptions let you leave error handling code out of much of your code. Exceptions are transmitted automatically through layers that have no knowledge of them, so you can write useful code that has no error handling logic at all. This helps keep the code straightforward and usable.
For example, compare two ways of writing the same simple procedure. With status returns:
STATUS DoSomething(int a, int b)
{
STATUS st;
st = DoThing1(a);
if (st != SGOOD) return st;
st = DoThing2(b);
if (st != SGOOD) return st;
return SGOOD;
}
And then with exceptions:
void DoSomething(int a, int b)
{
DoThing1(a);
DoThing2(b);
}
Even if the first version is rewritten with macros to hide much of the status return scaffolding, it’s uglier and more cluttered than with exceptions:
#define TRY(s) { STATUS st = (s); if (st != SGOOD) return st; }
STATUS DoSomething(int a, int b)
{
TRY(DoThing1(a));
TRY(DoThing2(b));
return SGOOD;
}
If the code were more complex than this, the extra noise from the error handling would be much worse. Exceptions keep the code clean.
Valuable channels
With status returns, a valuable channel of communication (the return value of the function) has been taken over for error handling. Some methods are so simple, and conceptually return a value, so human temptation takes over, and the method is written to return the value rather than a status code. “It’s a simple function, it can’t fail, this will be more convenient”. Over time, the code grows, and the method gets larger, calling more helper functions, and pretty soon it can fail, but it has no way to express it.
So the return value is overloaded: “If it fails, it returns NULL”. Now we have more than one convention in the code (probably more, because your numeric getters will return -1 if they fail), and everything still has to be checked. Your previously failsafe function now has to have all of its call sites updated to check for the new error value.
Why use a technique that begs to be subverted in the first place? Exceptions stay in the background, leaving the most useful tools for the successful cases. Functions can return values, and still have a useful way to fail.
Richer error information
Status returns are typically an integer. A few bits are reserved for flags (for example, a severity indication), and the rest are a large set of enumerated failures. This is a fairly impoverished error value. For example, suppose the failure is that a file could not be found. Which file? A status return can’t convey that much information.
Other channels can be developed to carry supplemental information, but typically they are not. In a status return world, the best you can hope for is for the failure site to log a message, and then to return the status. This is simplistic: perhaps the caller knows that it is OK for a file to be missing. If the file opener logs a message, the log will be incorrect (an error will be printed when nothing is wrong). If the file opener doesn’t log a message, the caller (or his caller) may have no way of getting the detail on the error needed to print a useful message.
Exceptions are instances of classes, and as such can carry as much information as they need to accomplish their task. Because they can be subclassed, different exceptions can carry different data, allowing for a very rich zoology of error messages.
As an example of the richness exceptions can express, Java defines exceptions as containing a reference to another exception, so that chains of effect can be constructed and retained as the exception moves up through layers of handling. This allows for rich diagnostic information: exception B occurred here, and was caused by exception A occurring there.
Implicit code
Status returns can’t even be used with some functions. For example, constructors don’t have an explicit return type, and so cannot return a status code. Destructors may not even be explicitly called, never mind that they don’t have a return value. In C++, operator overloading and implicit casting (controversial though they are) are other forms of implicit function calls that cannot be checked for return values.
None of these functions can be given status returns. If you don’t use exception handling, you have to either come up with some other way of marking errors within them, or pretend that they cannot fail. Simple code may be fail-safe, but code always grows, adding opportunities for failure. Without a way to express the failure, your system will only grow more error-prone and mysterious.
Sins of omission
Consider what happens in each technique when a coder slips up.
When a status return goes unchecked, a failure in the called routine will be undetected. The code will continue executing as if that operation had succeeded. There’s no way to characterize what might happen at that point, but it is clear that no one will know that an error had occurred. Perhaps the code will visibly fail later on, but that could be many operations later. How will you trace the problem back to the original failure?
If an exception goes uncaught, the exception will travel upward in the call stack either to a higher catch block, or to the uppermost frame where the operating system will do something with it, usually present it to the user. This is not good behavior for the system, but it is visible. You will see an exception, you will be able to diagnose where it was thrown, and where it should have been caught, and you will be able to fix the code.
I’m not covering here the problem of failing to announce a problem (either by returning a failure code or throwing an exception), because that case is a wash for the two techniques. Both are prone to it, and both will fail in similar ways.
So for human error, it comes down to this: human error with status returns results in invisible problems, human error with exceptions results in visible problems. Which would you rather have?
Counter-arguments
Joel Spolsky has argued that status returns are better. His main argument is that exceptions “are significantly worse than gotos”:
- They are invisible in the source code. Looking at a block of code, including functions which may or may not throw exceptions, there is no way to see which exceptions might be thrown and from where. This means that even careful code inspection doesn’t reveal potential bugs.
- They create too many possible exit points for a function. To write correct code, you really have to think about every possible code path through your function. Every time you call a function that can raise an exception and don’t catch it on the spot, you create opportunities for surprise bugs caused by functions that terminated abruptly, leaving data in an inconsistent state, or other code paths that you didn’t think about.
This seems like a reasonable argument until you work out what the code would look like with status returns. We aren’t arguing here whether functions should be able to fail or not, just what should happen when they do. So all of those possible exit points for a function are still possible exit points, but you have to check the status returns explicitly, and return from the function. So you’ve traded implicit complexity for explicit complexity, which may not be a good trade. With explicit complexity, you can’t see the forest for the trees. Your code is cluttered with the explicit handling of error statuses.
When presented with this explicit complexity, programmers will strive to reduce it. They have two primary ways to do it: hide the error handling, or omit the error handling.
Hiding the error handling is what we did above with the TRY macro. This simply turns your explicit code paths back into implicit code paths, but with the annoying noise of TRY littered all over the place. This is certainly no victory for the “explicit code paths are better” argument for status returns. If you’re going to use implicit code paths, at least use exceptions to create them so you have some modern tools at your disposal.
The other solution to the overwhelming explicit complexity in code paths is to simply avoid checking the error returns. Developers will convince themselves (through code inspection or system-level understanding or just plain bad logic) that a certain function always succeeds. This leads to errors going unchecked, which leads to invisible problems.
In a nutshell
Status returns are difficult to use. There are places where they are impossible to use. They hijack a useful channel of communication. For all of these reasons, it is easy and tempting to not use them. When not used, they produce silent failures in your system.
Status returns are inferior to exceptions. All modern programming systems provide tools for exception handling. Use them.
See also
- Exceptions in the rainforest, about the layers of real code, and how exception handling plays out in them.
- Asserts, about making assertions about the correctness of your code.
- Fix error handling first, about ensuring your error handling code is running its best.
- Log message style guide, about writing good log messages.
- My blog, where other similar topics are discussed.
Comments
STATUS DoSomething(int a, int b)
{
STATUS st;
st = DoThing1(a);
if (st != SGOOD) return st;
st = DoThing2(b);
if (st != SGOOD) return st;
return SGOOD;
}
...
STATUS st;
st = DoThing1(a);
if (st != SGOOD) cout
Actually, I wanted to point that you forgot the whole try/catch mess in your exception example...
Exceptions does not keep code as clean as you try to show.
The answer clearly depends on circumstances. Errors should be as visible as possible during debugging and testing - and invisible (if possible - and logged) when used by an end-user.
I think the general idea of exceptions is good but... To crash by default: Who did come up with such a bad idea?
In my opinion, the best discussion on exceptions starts with Tom Cargill's "Exception Handling: A False Sense of Security". When this article was first published (in '94) it was apparent to the C++ community that exceptions and their implications were not well understood.
The challenges he posed were eventually addressed on comp.lang.c++.moderated in the "Guru of the Week #8" thread (http://pierre.aubert.free.fr/divers/gotw/gotw008.htm). And I find Herb Sutter's _Exceptional_C++_ to be the best summary of the entire discussion.
My personal conclusion (I'm a fan of exceptions when they are properly used) is just a reinforcment of what should be standard programming technique; "functions should only do one thing".
void InstallSoftware(int a, int b)
{
FileCopier a;
RegistrySetup b;
// If we got this far without an exception
// then everything is ok
a.success();
b.success();
}
The idea is that a can undo anything it did, and will do so if you don't call a.success();
If any exceptions are thrown while the registry is being set up then a.success() isn't called and its destructor will remove all the files it installed.
void InstallSoftware(int a, int b)
{
FileCopier f(a);
RegistrySetup r(b);
// If we got this far without an exception
// then everything is ok
f.success();
r.success();
}
Second, what bugs me is that there is little consistency in error reporting methods. Does it throw an exception? Does it return zero on error? Does it return non-zero on error? Does it return negative on error? Do I need to check a global runtime variable for the error code? Do I need to call another function to get the error code? Which function should I call? Does it use setjmp/longjmp? Does it throw a structured exception?
Third, exceptions have their own traps. Don't go letting your destructors propagate exceptions. You'll probably be surprised what throws clauses do in C++. Should you catch(E e), catch(E *e) or catch(E &e)? Are you checking new's for null? Time to break out the Meyers and Sutter books...
Of course, you could also talk about checked exceptions, another hotly debated topic...
Both methods suck, frankly. At least Java makes an attempt to make exceptions better/safer.
Years ago at Lotus one of the architects did an actual benchmark of some code in 123 written first with the status technique and then with the exception technique, and claimed that the exception technique was 15% less code (he was comparing compiled code size) and maybe 5% faster.
I can predefine my own exception objects, store any pertinent information in the object that may help me debug the code, or find a way to handle the exception gracefull.
So I really have both...I can use exceptions and return usefull info on the exception.
Uh--I think you missed the point. The point is that the code with exceptions will do something on an error whether you catch the exception explicitly or not. (In this example, he doesn't, meaning it would probably be caught by the OS and the program would be shut down.) In the example with status codes, if you didn't check the return code, it will trundle merrily on its way, even if something went horribly wrong.
As to which approach is better, I'm not sure--both sides have valid points. I just wanted to point out that your comment ('This tells me everything I need to know about the depth of your argument') is by no means a deep argument in itself. All it demonstrates is that you either didn't understand or just didn't bother to really read the article.
There typically are 10 kinds of people, those who think in binary and those who don't... oops, throw XInappropriateMetaphor.
Anyway, I think the religious disagreement is between people that prefer explicit handling of errors vs. those that prefer implicit handling. The reason I say this is that if you're in the explicit camp, exceptions are even more bloated than status returns (both in clutter and execution overhead), which removes a significant fraction of their advantages.
I tend to live in the explicit camp, personally. But that said, it's perfectly clear to me what problem exceptions are intended to solve.
But consider this: either a piece of code knows what should be done if a function it calls fails, or it doesn't. If it does, it can, and probably should, handle the error, to avoid too much "action at a distance". If it doesn't know what should be done, there's not much point in it checking for or handling the error. Passing the buck just introduces unnecessary implementation coupling.
Of course, that just recasts the debate into terms of whether it's better to crash or fail softly when an error occurs.
STATUS DoSomething(int a, int b)
{
STATUS st=SGOOD;
if (st == SGOOD)
st = DoThing1(a);
if (st == SGOOD)
st = DoThing2(b);
return st;
}
Obviously, you could omit the first check for status.
What two things do you mean when you say "I really have both"? Exceptions already come with richer information (there's a section in the article about this).
Functions should only do one thing. If they don't then proceed at your own risk.
I don't know how much code I have reviewed (and developed in the past) that has not met this criteria to the detriment of readability and maintability; and of course reusability.
Even in Joel's modified trival example, it's copyfiles() that should be rolling back what it called... hopefully copyFile()
I use both statuses and throw exceptions depending on the situation. Especially on private functions I tend catch exceptions and return statuses, and then on public functions handle statuses and throw exceptions. The assumption is that I know how my privates are used, but obviously not the publics.
void ThrowExceptionFunc()
{
static int ex;
int i=0;
throw ex;
}
( loop 1 million times )
{
try
{
ThrowExceptionFunc();
}
catch( int ex )
{
}
}
Compiled with VS.NET, Release build, if you comment out "throw 1", this executes in 0.002 seconds. If, on the other hand, you throw an exception, this code takes over 4 seconds. Throwing exceptions is a performance hit.
In "More Effective C++", Scott Meyers writes "Compared to a normal function return, returning from a function by throwing an exception may be as much as three orders of magnitude slower. That's quite a hit... If, however, you've been thinking of using exceptions to indicate relatively common conditions... now would be an excellent time to think again."
So, exceptions should ONLY be thrown under exceptional conditions - not for "just thought you should know" messages.
I don't like status values either. I prefer the method of writing a small class which holds error information. You have to know something about performance to make it work efficiently, but you can impliment all kinds of great warning, error, and success messages which don't require the performance overhead of exceptions and are more useful than a status number.
When an exception occurs in layer A it may only be of concern to layer C. For example, C may know how to handle it, while B can't. A solution without exceptions would mean the error condition will crosscut B's implementation. This is also a reason why RuntimeExceptions in Java are better, because you aren't forced to check the exception or declare it in the throws clause.
(The language extension AspectJ provides better support for exceptions and allows the "softening" of need-to-handle exceptions.)
SetLastError();
return;
or
return x;
or
throw "Exception";
or some other mechanism of notifying the caller.
I still have an exit point wherever I would discontinue current function execution. Multiple exit points are just a prevalent in non-exceptional :) code as it is with exceptional code. Even with exceptions its possible to minimize exit points from any function with good design.
As to his first argument, I agree with Ned and one of the previous posters. If exceptions are clearly declared, meaning you are using a compiler that understands declaration of exceptions that can be thrown from any function, nothing is hidden from the programmer. I would say it is just the opposite. With clearly declared exceptions the programmer can easily see the complete list of all possible error conditions from a given function.
int doStuff() {
if (!createDirectories)
goto err_create_directories;
if (!installFiles())
goto err_install_files;
if (!updateRegistry)
goto err_update_registry;
return SUCCESS;
err_update_registry:
undoUpdateRegistry();
err_install_files:
undoInstallFiles();
err_create_directories:
undoCreateDirectories();
return ERROR;
}
Otherwise, how do you undo all of those things?
With return status checking, you know where you are. The exception example should add the appropriate code to determine this - thereby making it as messy as the return-status-checking theory.
As for context, you can always do catch-add_context-rethrow. Works for me.
Yes, exceptions have a performance hit, and that's not necessarily a bad thing. If you have a function that will fail regularly enough to cause a performance hit with exceptions, then it's likely what's causing it to fail isn't very "exceptional", and status codes should be used.
Joel's points against exceptions sound rather well reasoned at first, but they aren't.
Invisible exceptions are no more problematic than invisible status codes. Just as you may not know the exceptions a function may use, you just as well may not know the convention for returning error codes, especially when the function is returning a useable value as well. It doesn't matter whether you use status codes or exceptions, neither avoids the necessity of proper documentation.
Joel's second point is almost moot in itself. As Ned explained, an exception that isn't immediately caught will propagate until it is, even if that happens to be at the OS level. If a function leaves data in an inconsistant state, then your destructors need work. In addition, the ability of exceptions to propagate between layers without introducing dependancies, mentioned by Macneil, is half the benefit of their use.
In response to Warren, yes, handling exceptions from reoccuring calls will introduce more code in situations where context is important. Even ignoring the probably necessary refactoring of said function, the additional code is no more than what you'd have with status checks, and retains the benefits of exceptions.
This isn't a religious issue, it's a technical one. One solution behaves one way, the other in a different manner. Research and evaluate the behaviour of each, and decide for yourself.
But it has it's own catch.
Try to create an exception object when the exception was "out of memory".
And try to solve Mike Mangino's goto/status example with exceptions.
Is there "one thing fits all"?
Pre-create the exception at the beginning of the program and throw it only if the event arises.
> And try to solve Mike Mangino's goto/status example with exceptions.
Something like this would work:
void doStuff() {
try {
createDirectories;
try {
installFiles;
try {
updateRegistry;
} catch (Exception e) {
undoUpdateRegistry;
throw e;
}
} catch (Exception e) {
undoInstallFiles;
throw e;
}
} catch (Exception e) {
undoCreateDirectories;
throw e;
}
}
Nested try/catch? Is that better, than nesting "if"?
And when you have 5-6? Can you read/debug properly?
Is there anything wrong with nesting "if" statements and what has this got to do with exceptions?
> And when you have 5-6? Can you read/debug properly?
When the code is properly laid out (unlike in this web forum) you can easily see the paths of execution that might transpire.
But I think you're missing the point: yes, if you want to have several operations, each of which might fail and each of which requires clean-up if they do, you'll need a relatively complex construct. Ultimately, you should have coded the operations themselves to catch their errors and clean up so that they could be used elegantly. So you'd then have:
void InstallFiles() {
try {
// Whatever
} catch (Exception E) {
// Cleanup
}
} ... and so on, for each function, then you can just go:
InstallFiles;
UpdateRegistry;
CreateDirectories;
The benefits that exceptions provide even in this case remain numerous:
- Errors can be easily propagated to the level at which they are best dealt with
- Obscure status code values do not have to be memorised yet errors cannot be accidentally ignored
- You can write the code to deal only with specific exceptions that you might expect to happen (eg, FileNotFoundException) and yet it will still operate in the case of less-common exceptions such as MemoryFullException by passing the error on automatically
- Additional information can be tied to the Exception (eg, a FileNotFound exception can store a filename where a status code of E_FILE_NOT_FOUND cannot)
The transparency of exceptions is a benefit, not a problem. A great percentage of the lines of code in a program can fail and dealing with every single circumstance individually complicates the program significantly, making it harder to understand. It also has the effect of complicating testing to the point where it might become almost impossible.
For instance, imagine code which creates two new objects:
Object o = new Object;
Object p = new Object;
Both of these calls could fail: there might not be enough memory, the object's constructor might throw an exception, etc. If these objects are particularly liable to fail, armed with exceptions, a programmer can choose to catch the exceptions and deal with them specifically - but if the chance of the functions failing is extremely remote or there is little they can realistically do if they _do_ fail, they can ignore them at this level and allow them to be dealt with by a higher level handler.
Without exceptions, checking return values becomes complusory if reliability is to be maintained. The two simple lines above become:
Object o = malloc(sizeof(o));
if (!o)
; // Do what? Quit? That's useful..
Object p = malloc(sizeof(p));
If (!p)
; // Do something imaginative
Now imagine having to write a test module that arranges things such that the creation of Object o succeeds and Object p fails and you begin to see how explicitly dealing with all possible errors might actually decrease reliability of programs.
The argument that functions have multiple possible exit points with exceptions does not hold because in the majority of cases, functions which check return values will bail out in the case of errors anyway. Exceptions consolidate the error-handling code for all cases into "catch" handlers and in fact probably serve to decrease the number of code paths that could be taken.
Unfortunately, your site picked up that fact and highlighted in different colored backgrounds the seached for words -- making your page irritating to read at best. DON'T DO THAT!
For example, some programmers prefer to use global state variables or functions.
( For an example, see OpenGL )
Moreover, some ( professional ) programmers don't even bother checking for errors.
( Hey it either works or it doesn't work. )
For an example of this type of coding,
see the quake 3 source code.
You use exceptions for exceptional situations. For example, out of memory rarely happens and the function allocating memory can rarely do something intelligent with it anyways, so exceptions are a win. On the other hand, lookup in a hash table can and will fail often. Much better to return a Maybe (Haskell), option (SML), past-the-end iterator (C++), null (Java), or similar instead.
Just like everything else that's a (holy) war, neither is always better. Use the one that fits better and you'll be better off than anyone that thinks one is always better.
Status codes are always going to clutter up your code with explicit error handling in a way that exceptions don't, but explicitly declaring the exit points in your code is much better than only knowing them implicitly.
As far as hijacking the return value, I don't. In C#, I return a struct: Result. You wrap the result object around your return value (if any), and it returns back all nice and strongly typed.
Result ri = obj.DoStuff();
if (ri.Error) { // propagate error }
int myValue = ri.ReturnValue;
Yes, int myValue = obj.DoStuff(); is certainly cleaner. But it's easy to miss pieces of state that need to be cleaned up if you don't handle error cases explicitly. Exceptions leave subtle time bombs that go off in your code and break the application state. Error handling gives you an explicit place where you need to clean up anything interesting that happened in your method before exiting.
That should be Result<T> and then
Result<ri> = obj.DoStuff();
Or, rather,
Result<int> ri = obj.DoStuff();
Exceptions allow for poor programmers to do really nasty things, it is similar to why most programming courses tell you never to use gotos, its not that gotos can't be used effectively, and same with exceptions, its just it requires a lot of thought and planning, and you making your own rules involving their use.
I do not like your first snippet, I would do it like this:
STATUS DoSomething(int a, int b)
{
STATUS st;
st = DoThing1(a);
if (st == SGOOD)
{
// only do thing 2 if thing1 is good
st = DoThing2(b);
}
return st;
}
If you were to add more "things" and needed to different things when different errors occured you'll find the status technique is a lot easier to follow.
From my experience it really depends on what I'm doing, in general status's make it harder to whip out code really fast, and error handling code is more spread out, but it does result in less buggy code. If I am writing short scripts to do work for me, exceptions are awesome, I love them! You hardly need to write error handling code because you will get a nice error message to the screen when it crashes, but if you are writing a big system that will cause a catastrophe if it crashes and doesn't have a user sitting there watching the screen then using exceptions can be really tricky, most people can't do it, and it will result in buggy code. Imagine the code that controls airplanes was written using exceptions, the thought of that scares me.
Absolutely not.
Create an enum and throw this enum, catch and do a switch( my_error_enum ), which by nature cascades through (unless on break), and cleanup in reverse order of the function calls.
I wouldn't hire anyone that used goto's, or returned error values. Ever.
i'm with Joel.
the sample should be more like:
STATUS DoSomething(int a, int b)
{
STATUS retVal = DoThing1(a);
if (retVal == SGOOD)
retVal = DoThing2(b);
return retVal;
}
This is absolutely fine, especially if someone else wants to
change or extend it in the future, because it is
obvious what to do e.g.:
STATUS DoSomething(int a, int b)
{
STATUS retVal = DoThing1(a);
//this works always!!!
DoSomethingImportant(a);
if (retVal == SGOOD)
retVal = DoThing2(b);
return retVal;
}
that was easy, wasn't it?
Now lets have a look at Exception base code:
void DoSomething(int a, int b)
{
DoThing1(a);
//this does not!!! works always
DoSomethingImportant(a);
DoThing2(b);
}
In a big Project it is absolutely standard that different people manage the same code within several years.
This has nothing to do with skillsets... but just only giving you the possibility to manage and extend
your code fast without checking the hole thing over and over again for a small change. like that.
To make an extension like that in the sample above with exception based code you will have to rethink the whole
function and why and how the functions depend on each other.
However the problem is not only about redesigning the exception based functions here to make it work, the bigger
problem is, that the person adding DoSomethingImportant(a); does not know about the other stuff and
introduces a bug that happens only sometimes depending on something totally different.
This just gets worse when you span the sample above over a hierarchy of methods.
The "DoSomethingImportant" should properly be put into a finally block.
Throwing an exception should be considered in situations where the function/code cannot complete the job it is designed for.
Return values should be used for returning data to the caller.
i think it is very difficult to define generally what is meant with:
"function/code cannot complete the job it is designed for"
The mentioned STATUS return value might be a Result (a custom class) containing status and return value, which btw is a pattern that works also between process/layer boundaries.
With exceptions:
*You can check them.
*If the function fails and you don't check them your program will crash and say something that the user will not understand anyway.
With error numbers and returns:
*You can check them.
*If the function fails and you don't check them your program will either:
*crash and say something that the user will not understand anyway (likely)
*crash silently (also likely)
*complete execution successfully or semi-successfully
*seriously and actively mess up something
I won't reiterate all the points I've already made. I will point out that you talk about adding information as the error makes its way up through the callers, but then you show an example that doesn't do that. The kind of annotated error handling that you are advocating is very difficult to do with status returns, and common with exception handling.
Add a comment: