Exceptions in the rainforest
Created 16 October 2003
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
- Exceptions vs. status returns, which lays out the arguments in favor of exceptions.
- Fix error handling first, about ensuring your error handling code is running its best.
- My blog, where other similar topics are occasionally discussed.
Comments
(Bob's appreantly has no permalinks)
Geez, everybody is piling on Joel.
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;
}
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;
}
}
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.
Straight from the horse's mouth:
"Primary tropical rainforest is vertically divided into at least five layers..."
void InstallSoftware()
{
Log log;
try {
CopyFiles(log);
MakeRegistryEntries(log);
}
catch (CException & ex) {
Rollback(log);
throw ex;
}
}
Just use right techiques in right places, guys
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.
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" !!!
Thanks Before
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...
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.
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...
Maintaining code over time, often also with several programmers requires code consistency and clarity - SUPER VITAL, but rarely done adequately let alone well! On the clarity (which naturally breads reliability) side of thing is where error handling/recovery is HUGE, HUGE, HUGE, and did I forget to say HUGE deal! Delphi has gifted us with a superb exception handling system. So after all that said, here is what I found to be the case for any decent sized app (something like 10k+ lines of code, smaller apps matter less and less the smaller they are - APP SIZE MATTERS):
Use Exceptions, and DO NEST them as appropriate for both the user and your bug tracing, - your life will be so much easier, and your end users will like your app much more!!! ...e.g.:
procedure TClass42.Thingy(const pFile: string);
begin
try
...some code
except
on E: Exception do
raise Exception.Create(Format('Our thingy operation for file %s failed because: ', [pFile]) + #10 + E.Message)
end
end;
1) If you want reliability, forget about Error Status.
2) If you want code clarity, forget about Error Status.
3) If you want flexibility in how and where to handle errors, forget about Error Status.
4) In your mind DO NOT mix things that functions like IndexOf() return, with Exceptions. Returning a boolean, 0/-1, nil or enums is NOT an exception, it is a data result!
5) Any test functions (i.e. IsNull) are the same as in 4) above.
To know why 1-5 are true, most of the comments from others here will tell you!
Add a comment: