As part of a debate about exceptions and status returns, Joel asked for an example of exception handling using a particular chunk of code. Before jumping to the code, I want to talk about rainforests for a little bit. If you haven't read my previous article about exceptions and status returns, you might want to start there.

Rainforests

If you've ever studied the rainforest, you know that it is not a simple place. A simplistic model of it would be that there are lots of trees, and lots of animals, and they all live together. It's more interesting than that: The forest is divided horizontally into layers, and each layer has its own ecosystem, with different inhabitants. To understand how the rainforest works, you have to consider the layers separately, and see how they differ from each other.

Complex software is the same way: there are different layers, and the error handling they perform is different. If we want to discuss what exception handling looks like in real code, we have to talk about the layers.

Three layers of code

In my experience, there are three layers to real code (from bottom to top, so this list might look upside-down):

  • Adapting the software beneath you.
  • Building pieces of your system.
  • Combining it all together.

Keep in mind, this is a simple model, and real software is fractal in most of its aspects. A 100,000-line system will have layers within layers within layers. But this three-layer model closely matches the way I've seen a number of real systems evolve. Let's look at each of these layers in detail.

Adapting the software beneath you

Beneath every piece of software is more software. Your Windows application sits on top of the Win32 API, or ATL. Your PHP web site sits on top of MySQL calls, and PHP primitives. Your Java system sits on top of the JDK, the J2EE facilities. Even if you are writing a device driver, your code is sitting on top of the actual I/O operations that write bits to the disk, or whatever it is your driver does.

At the lowest layer of your system, your code deals with your particular underlying software. It makes its calls, and interprets the results. This layer is where you convert cultures, making the underlying software more the way you'd like it to be: operations become more convenient, concepts are presented more palatably to the rest of the system, ugly workarounds are hidden.

Building pieces of your system

The middle layer of your code is where you construct the pieces of your world. Are you writing a spreadsheet? You'll need a cell engine, and some way to read and write data files, and connectors to databases, and charting modules. In some worlds this is called business logic.

This is where the bulk of the code will be, and where you are likely to be adding value. Few applications compete on how well they read and write the registry. The interesting technology is in the cell engines, or drawing paradigms, or database intelligence, or logical inference algorithms. This is the interesting part. The more time you can spend here productively, the better off you will be.

Combining it all together

At the top of your system is the big picture. For example: when the application starts, we need to create an empty document, initialize the database layer, and show the GUI. This is where you can see the main flow of the application. If you had to explain what your system did in detail to a knowledgeable user, this layer is the one you'd be talking about. This is the stage manager layer, coordinating pieces, making the whole thing hang together into a cohesive whole.

How exceptions are used in the layers

At the bottom layer (Adapting), there's a lot of throwing exceptions. Unless you are coding in Java or C#, where the system toolkits throw exceptions (in which case, I'm preaching to the choir), the layer beneath you more than likely is returning statuses to you. Each call will have its return value checked, and converted into an appropriate exception, and thrown. Sometimes, error values will be dealt with immediately. For example, this layer may implement some simple retrying mechanism for some operations, or it may decide that some error returns are really not errors.

At the middle layer (Building), things are flowing pretty smoothly. Typically, there's not a lot of exceptions being thrown, and not a lot being caught either. This is where you often get to just think about the ideal case, and focus on the algorithms and data structures at hand. Of course, exceptions can happen, especially in the A-layer calls you make. But for the most part, you can let those exceptions fly. An upper layer will deal with them.

At the top layer (Combining), there's a lot of catching exceptions happening. Couldn't open a file? Now you have to decide what to do about it. You can alert the user, try a different file name, exit the application, whatever you as the system designer decide is the best approach.

This C-layer code can actually be quite pre-occupied with dealing with exceptions. This makes sense: this is the layer where the code really knows what's going on. If you have an A-layer function to open a file, what should it do when the file can't be opened? How can you possibly say? This function will be used to open all sorts of files for all sorts of reasons. Maybe the C-layer caller knows that the file could be missing, and has a plan for what to do in that case, so alerting the user would be wrong. It's the C-layer that understands the big picture, so it's the C-layer that should be dealing with the exceptions.

Exceptions vs. status returns again

Now for Joel's example. He asked that we discuss this code:

void InstallSoftware()
{
    CopyFiles();
    MakeRegistryEntries();
}

Using the three-layer model above, this is clearly C-layer code. I know Joel asked for this example because he knew that even with exceptions the code would be cluttered with error handling, just as it would be with status returns. He's right. It's C-layer code, so it will have to deal with unusual cases. There's no way around that.

Others have taken up this challenge, and come up with some nice ways to deal with it cleanly, using C++ destructor semantics to ensure that operations are rolled back. To be perfectly honest, I don't know that I would have been as clever as these writers, though they have given me some good ideas. I might have done it like this:

void InstallSoftware()
{
    try {
        CopyFiles();
        MakeRegistryEntries();
    }
    catch (CException & ex) {
        RemoveFiles();
        DeleteRegistryEntries();
        throw ex;
    }
}

This function either succeeds, in which case the files are copied and the registry entries are written, or it throws an exception, and the files and registry entries are cleaned up. Is this sufficient? I don't really know, and in a real implementation I can imagine it getting much hairier than this.

The status return folks may well be crowing about this code, that it is either not handling the problems completely, or that it is just as ugly as status return code. They're missing the point. I'm not claiming that exceptions make all code prettier, or that they somehow remove the burden of thinking through what should happen when something goes wrong.

The debate over exceptions and status returns is not about whether error handling is hard to do well. We all agree on that. It's not about whether exceptions make it magically better. They don't, and if someone says they do, they haven't written large systems in the real world.

The debate is about how errors should be communicated through the code. The C-layer code we're talking about is going to be complicated no matter which technique you use to communicate errors around.

But what does the B-layer code look like?

void MakeRegistryEntries()
{
    CRegistry reg;

    reg.WriteString("ProductName", "Ned's FooBar");
    reg.WriteString("Version", "1.2b");
    reg.WriteDword("WebUpdateInterval", 7*24*60*60);
}

Here at the B-layer, we can get into the zone and just write registry entries. How would this look with status returns? Either cluttered with if statements, or hidden behind macros that simply pull your code into the "hidden function return" camp that are supposed to make exceptions evil.

The A-layer code looks like this:

void CRegistry::WriteString(
    const char * pszValueName,
    const char * pszValue
    )
{
    ASSERT(m_hKey != NULL);

    DWORD cbData = (DWORD)(strlen(pszValue)+1);

    LONG lRet = ::RegSetValueEx(m_hKey, pszValueName, NULL, REG_SZ, (BYTE*)pszValue, cbData);
    if (lRet != ERROR_SUCCESS) {
        CWin32Exception ex(lRet);
        throw ex;
    }
}

Here we're adapting to the Win32 registry functions, converting their status returns into exceptions (which carries the actual status return as data so that it can be used for error messages, or analysis).

These example are all too brief to be real code, but demonstrate the concepts. Broadly speaking:

  • A-layer generates exceptions,
  • B-layer can often ignore the whole issue, and
  • C-layer decides what to do

Exceptions are better at communicating errors

The challenge in building a large system is making sure errors get communicated around. Exceptions are a better way to do that than status returns:

  • Exceptions can carry richer information. If error handling is so important, why try to cram it all into a DWORD?
  • Exceptions let the B-layer get on with its work without being a mindless bucket brigade for status returns.
  • Exceptions make human error (failure to catch) visible, while error returns make human error (failure to check) invisible.
  • Exceptions leave the primary channel (function returns) available for the primary work.

See also

Comments

[gravatar]
andrew 11:53 AM on 16 Oct 2003

Bob gave this some treatment over at his place: http://www.bobcongdon.net/blog/

(Bob's appreantly has no permalinks)

Geez, everybody is piling on Joel.

[gravatar]
andrew 11:58 AM on 16 Oct 2003

Also, in your example to Joel, don't forget our "real world" status (or should I say STATUS) experience:

STATUS DoSomething(int a, int b)
{
STATUS st;
if (st != DoSomethingEx(a,b))
goto error;
if (st != DoSomethingEx2(a,b))
goto error;
error:
return st;
}

[gravatar]
Ross 1:05 PM on 17 Oct 2003

I know it's an example and that it's hypothetical BUT

Using the following example (taken from the article) what happens if anything in the catch{} block throws an exception (as well it might if CopyFiles() threw the exception and then you called DeleteRegistryEntries).

What is the solution? try{}catch{} in the catch{} block ?

void InstallSoftware()
{
try {
CopyFiles();
MakeRegistryEntries();
}
catch (CException & ex) {
RemoveFiles();
DeleteRegistryEntries();
throw ex;
}
}

[gravatar]
Wayne 9:39 PM on 17 Oct 2003

Ross, taking your point to an even higher level, ultimately there are errors that software just can't fix or clean up from. For example, what if during the RemoveFiles() function call in the example there was a permanent hard disk failure?

The best that a program can do here is to attempt to accurately inform the user of the issue exit.

That's why exceptions are cleaner in that you can wrap one highest level exception around all your code to catch anything, report it to the user, and exit. Doing that with status returns can get extremely messy. I know, because it's all I used to do before exceptions were made mainstream.

But it's Joel's point that this type of catch-all exception handling leads to a higher probability of being sloppy and missing exceptions that could be readily recovered from.

[gravatar]
Joe 9:28 AM on 20 Oct 2003

BTW, the forest is divided into *vertical*, not horizontal layers :)

[gravatar]
Mr. Pedant 2:22 PM on 21 Oct 2003

Just to be pedantic, the rainforest is divided vertically into horizontal layers.

Straight from the horse's mouth:
"Primary tropical rainforest is vertically divided into at least five layers..."

[gravatar]
Doug 1:42 AM on 23 Oct 2003

Error codes are 'opt-in', while Exceptions are 'opt-out'. That is, you have to explicitly code to allow error codes to propagate (opting in), while with exceptions, you can have gobs of code that is not cluttered with the exception handling code, and only write handling code in places where you can actually do something about it. Most code is unable to do much about the exception - imagine trying to handle a FileNotFoundException in middle-layer code, how would you prompt the user? You wouldn't, so it is a UI-layer problem. Exception handling is usually cleaner overall, because only code that cares to handle and can handle the exception needs to get 'cluttered' to actually handle it. All the other code remains uncluttered. Thus, the opt-in nature of exception handling tends to result in more readable code and more cohesive programs.

[gravatar]
Andrey Platov 12:12 PM on 27 Oct 2003

Hi all, Joel came with absolutely wrong sample and answer to Joel should be:

void InstallSoftware()
{
Log log;
try {
CopyFiles(log);
MakeRegistryEntries(log);
}
catch (CException & ex) {
Rollback(log);
throw ex;
}
}

Just use right techiques in right places, guys

[gravatar]
Ben W 3:52 PM on 4 Nov 2003

Ned, aren't you ignoring a big issue? Non-trivial code at the B level, which just lets exceptions fly by, has to be equipped with declarations of objects having destructors tending towards nightmarish complexity.

From the point of view of B level code, any function you call might return straight into your destructors, which have to tidy everything up without knowing how far through the main body of code you got.

(Unless you sprinkle flag-settings through the body of the code. But B level code is supposed to be clean.)

Or is this just an argument against trying to write non-trivial code in the first place? I know it's not pretty with status returns; but with exceptions, isn't it even worse? i.e. the body of the code looks very pretty, and the destructors are horrible.

[gravatar]
Matt Morris 10:02 AM on 6 Nov 2003

In C++, this is dealt with by acquiring resources in object constructors and releasing them in destructors. Objects are instantiated on the stack, and as they go out of scope, resources are freed. The nice thing about this approach is that there's no need to worry about how the function exits. See Bjarne's "appendix e" to his book (google for "c++ appendix e"). I have implemented this consistently through a large system and it works extremely well.

[gravatar]
Flay 7:55 AM on 30 Dec 2003

IMHO exceptions are bad because it makes execution flow a little bit unpredictable. It's like "goto" which we avoid.

Anyone tried to debug large systems with exceptions? We started at A, then down to B, then down to C, and then, bum-bang, voila, back to A! I hate this "jumps" !!!

[gravatar]
Flay 7:56 AM on 30 Dec 2003

Sorry, wrong sequence of A-B-C, should be: start form C, down to B etc.

[gravatar]
DBMaster 6:15 AM on 5 Oct 2004

can anybody give me java code and explanation about rainforest algorithm related to decision tree on machine learning
Thanks Before

[gravatar]
Ashwin Nanjappa 9:05 PM on 17 Apr 2009

I found this post very useful in understanding exceptions and their practical usage. Thanks for writing and sharing.

[gravatar]
Kevin 1:23 AM on 29 Apr 2009

"g disk usage analyzer" led me here however I enjoyed stumbling upon this vintage post of Ned's. The topic is so fundamental and I'm glad that people continue to find it helpful.

At the risk of re-igniting the discussion, the strongest argument I can offer in favor of A-layer adaptors from error status returns to exceptions is that it's all too easy to ignore an error return and more difficult* to ignore an exception.

I'm sure many readers (half a decade ago or since then) have spent remarkable amounts of time debugging some issue ultimately found to include one or more A-layer system or library calls which failed to check for error returns. Had checks existed symptoms of something amiss are often manifested much earlier in the development cycle, but even when surfaced late the problem areas are more clearly indicated.

As Ned discussed, diligently checking function call status returns is laborious and obscures program logic, it also often overloads the value types being returned. These aspects are unavoidable for A-layer code and to consider bubbling error returns up to the canopy is not only counter productive, but also unlikely to be performed reliably.

The non-inline handling feature of exceptions is a benefit in that it allows program code to more clearly represent success-path sequential logic. Resource initialization and lifecycle semantics still have to be managed in light of potential exceptions, however block scopes and stack-based instances of classes encapsulating resource handling do more than make this possible.

Even where there is consensus in a development team that error status returns are encapsulated in A-layer logic and converted to exceptions, the next potential pitfall relates to unnecessary or misbehaved catches or (*) even more egregious catch (...) {/*ignore*/} coding follies.

Often a B-layer exception handler can be replaced by automatic resource handling classes as mentioned, and other mid-layer catches will re-throw exceptions augmented with higher level context information (e.g. what type of file was being created when the access violation exception was raised). Languages with checked exception semantics can also make re-throws necessary.

Reviews of A-layer logic should ensure that function call error returns are checked and raise exceptions properly, and that B or higher layer exception handling isn't negating this value by silently ignoring problems.

Now back to he matter of analyzing disk resource consumption on my TV...

[gravatar]
David Terei 10:36 PM on 23 Jul 2009

I tended to agree with Joel at first but I think that was just a little of a knee jerk reaction. I think the main issue with exceptions isn't the concept itself but how it is generally used.

My biggest gripe with exceptions is when they are used to convey information which I don't really see as an exception but should instead be a return value. For example, an API I'm working with at the moment has a function called 'getUser(String);' which does what it says. I would think such a function can return two values, a user, or null if no user matching the string was found. Instead it will either return a user, or throw an exception if no user is found. I think this is an incorrect design, a null user should be a perfectly valid return value. It shouldn't be treated as an exception. An exception should be thrown for something more like a failure to connect to the backing database which stores user credentials.

I also quite like Haskell's error handling mechanisms. I think it generally has a nice mix between exceptions and error status codes. But the nice thing is it has a properly designed error status code mechanism. Returned objects can be wrapped in a Maybe type, which allows null returns to be specified, or they can be wrapped in an Either type, which allows either the correct result to be returned, or an error result to be returned. Also supports try/catch style exceptions.

Oh and another reason why a lot of programmers are against exceptions I think is primarily due to checked exceptions. And they are defiantly a bad idea.

[gravatar]
CheshireCat 11:06 PM on 16 Dec 2009

but exceptions was totally wrong idea.
and i hear here mainly wrong arguments,
except yours, Flay)) yes, exceptions
forces to loose control flow.

i now also throws some rocks in.

1)errors must not be "propagated" to other layers in way many of you suggest.
errors in all big programs must be fully managed in their separate error processing module/subsystem.
then A and C layers just use it. register as error sources and error acceptors, send and receive messages via software bus, and so on...
B layer is completely unaware from that.

it is not such complicated as sounds)
and such architecture is much-much-much more reliable than exceptions in the real world. world where big programs is constantly modified by gangs of mediocre half-time programmers.
just imagine - catching and throwing anybody's exceptions) add more exceptions) and more)... ?

2) error recovery must be done in transactional style _only_.
all-or-nothing.
one more argument having STM
support in language?
(first argument was easy parallelisation)

3) why you insist on packing all error codes to DWORD? just use normal language) with easy structure returns, they contains no less information than exceptions. C(++) is too baroque now as language))

exceptions is very error prone and not simplify anything. they only make _illusion_ of simplification.

exceptions ("modern GOTO") just
not needed anymore. completely.
it is just one more unnecessary source of troubles)) one more dead weight in any language now))

p.s. meanwhile, just is OOP. when you think about language semantic from the point of permanent modifications of program sources many cool features
dies in one moment))
just stop praising some cool boolsh.t and loudly yell - "inheritance considered harmful"!))
functional programming will be next mainstream.

and if managers hires only C...
so too bad for _that_ managers))
seems they die married on his legacy code...

Add a comment:

name
email
Ignore this:
not displayed and no spam.
Leave this empty:
www
not searched.
 
Name and either email or www are required.
Don't put anything here:
Leave this empty:
URLs auto-link and some tags are allowed: <a><b><i><p><br><pre>.