GotW #65

Home Blog Talks Books & Articles Training & Consulting

On the
blog
RSS feed November 4: Other Concurrency Sessions at PDC
November 3
: PDC'09: Tutorial & Panel
October 26: Hoare on Testing
October 23
: Deprecating export Considered for ISO C++0x

This is the original GotW problem and solution substantially as posted to Usenet. See the book Exceptional C++ Style (Addison-Wesley, 2004) for the most current solution to this GotW issue. The solutions in the book have been revised and expanded since their initial appearance in GotW. The book versions also incorporate corrections, new material, and conformance to the final ANSI/ISO C++ standard (1998) and its Technical Corrigendum (2003).

Try and Catch Me 
Difficulty: 3 / 10

Is exception safety all about writing try and catch in the right places? If not, then what? And what kinds of things should you consider when developing an exception safety policy for your software?

Problem

JG Question

1. What is a try-block?

Guru Questions

2. "Writing exception-safe code is fundamentally about writing 'try' and 'catch' in the correct places." Discuss.

3. When should try and catch be used? When should they not be used? Express the answer as a good coding standard guideline.

Solution

1. What is a try-block?

A try-block is a block of code (compound statement) whose execution will be attempted, followed by a series of one or more handler blocks that can be entered to catch an exception of the appropriate type if one is emitted from the attempted code. For example:

// Example 1: A try-block example
//
try
{
  if( some_condition )
    throw string( "this is a string" );
  else if( some_other_condition )
    throw 42;
}
catch( const string& )
{
  // do something if a string was thrown
}
catch( ... )
{
  // do something if anything else was thrown
}

In Example 1, the attempted code might throw a string, an integer, or nothing at all.

2. "Writing exception-safe code is fundamentally about writing 'try' and 'catch' in the correct places." Discuss.

Put bluntly, that statement reflects a fundamental misunderstanding of exception safety. Exceptions are just another form of error reporting, and we certainly know that writing error-safe code is not just about where to check return codes and handle error conditions.

Actually, it turns out that exception safety is rarely about writing 'try' and 'catch' -- and the more rarely the better. Also, never forget that exception safety affects a piece of code's design; it is never just an afterthought that can be retrofitted with a few extra catch statements as if for seasoning.

There are three major considerations when writing exception-safe code:

a) Where and when should I throw?

This consideration is about writing 'throw' in the right places:

- What code should throw? That is, what errors will we choose to report by throwing an exception instead of by returning a failure value or using some other method?

- What code shouldn't throw? In particular, what code should provide the AC (nothrow) guarantee? (See GotW #61.)

b) Where and when should I handle an exception?

This consideration is indeed about writing 'try' and 'catch' in the right places:

- What code could catch? That is, what code has enough context and knowledge to handle the error being reported by the exception (possibly by translating the exception into another form)? In particular, note that the catching code also needs to have enough knowledge to perform any necessary cleanup, such as of dynamic resources.

- What code should catch? That is, of the code that could catch the exception, which is best suited to do so?

Using the "resource acquisition is initialization" idiom can eliminate many try-blocks. If you wrap dynamically allocated resources in owner-manager objects, typically the destructor can perform automatic cleanup at the right time without any 'try' or 'catch' at all. This is clearly desirable, not to mention that it's also usually easier to code now and to read later.

GUIDELINE: Prefer handling exception cleanup automatically using destructors instead of try/catch.

c) In all other places, is my code going to be safe if an exception comes roaring through out of any given function call?

This consideration is about using good resource management to avoid leaks, maintaining class and program invariants, and other kinds of program correctness. Put another way, it's about keeping the program from blowing up just because an exception happens to pass from its throw site through code that shouldn't have to particularly care about it, before arriving at an appropriate handler. For most programmers I've encountered, it turns out that this is typically by far the most time-consuming and difficult-to-learn aspect of exception safety.

Notice that only one of these three considerations has anything to do with writing 'try' and 'catch'. And even that one can often be avoided with the judicious use of destructors to automate cleanup.

3. When should try and catch be used? When should they not be used? Express the answer as a good coding standard guideline.

Here's one suggestion. In brief:

1. Determine an overall error reporting and handling policy for your application or subsystem, and stick to it. In particular, the policy should cover the following basic aspects (and generally includes much more):

a) Error Reporting: Define what kinds of errors are to be reported using exceptions as opposed to other error reporting methods. Generally it's good to choose the most readable and maintainable method for each case by default; for example, exceptions are most useful for constructors and operators that cannot emit return values, or where the throw site and the handler are widely separated.

b) Error Propagation: Among other things, define the boundaries that exceptions shall not cross; typically these are module or API boundaries.

c) Error Handling: Among other things, mandate that owning objects and destructors be used to manage cleanup instead of try/catch, wherever possible.

2. Write 'throw' in the places that detect an error and cannot deal with it themselves. (Clearly, code that can resolve an error immediately doesn't need to report it!)

For every operation, document what exceptions the operation might throw, and why, as part of the documentation for every function and module. You don't need to actually write an exception specification on each function, but you do need to document clearly and rigorously what the caller can expect, because error semantics are part of the function's or module's interface.

3. Write 'try' and 'catch' in the places that have sufficient knowledge to handle the error, to translate it, or to enforce boundaries defined in #1. In particular, I've found that there are three main reasons to write "try/catch":

a) To handle an error. This is the simple case: An error happened, we know what to do about it, and we do it. Life goes on (sans the original exception, which has been safely put to rest). Again, do this in a destructor if possible; if not, go ahead and use try/catch.

b) To translate an exception. This means catching one exception that is reporting a lower-level problem, and throwing another that is couched in the context of the translating code's own higher-level semantics. Alternatively, the original exception can be translated to some other representation, such as an error code.

For example, consider a communications session utility class that works across many host types and transport protocols: An attempt to open a session to another host can fail for any number of low-level reasons that the session class can detect (for example, a failure to detect the network, or authentication/permission rejection from the remote host). The Open() function can handle these conditions itself, and there's no use reporting them to the caller, who after all has no idea what a Foo packet is or what to do if it Barifies; the session class handles its internal low-level errors directly, keeps itself in a consistent state, and reports its own higher-level error or exception to inform its caller that the session could not be opened.

void Session::Open( /*...*/ )
{
  try
  {
    // entire operation
  }
  catch( const ip_error& err )
  {
    // - do something about an IP error
    // - clean up
    throw Session::OpenFailed();
  }
  catch( const KerberosAuthentFail& err )
  {
    // - do something about an authentication error
    // - clean up
    throw Session::OpenFailed();
  }

  // ... etc. ...
}

c) To catch(...) on subsystem boundaries or other runtime firewalls. This usually also involves translating the error, usually to an error code or other non-exceptional representation. For example, when your stack unwinds up to a C API, you only have two choices: Return an error code right away for the current API function, or set an error state that the caller can query later using a complementary GetLastError() API function.

Summary

A wise man once said:

"Lead, follow, or get the blazes out of the way!"

In exception safety analysis, we might say instead:

"Throw, catch, or get the blazes out of the way!"

In practice, the last get-out-of-the-way case accounts for the bulk of exception safety analysis and testing. That's the major reason why exception-safe coding is not fundamentally about writing 'try' and 'catch' in the right places. Rather, it's fundamentally about getting out of the bullet's way in the right places!

Copyright © 2009 Herb Sutter