31

Two anti-patterns that are incredibly common in most code bases I've worked out of are Boolean return values to indicate success/failure, and generic integral return codes to indicate more details about an error message.

Both of these are very C-like and do not fit into C++ very well in my humble opinion.

My question is in regards to best practices when it comes down to designing exceptions into your code base. In other words, what is the best way to indicate the finite possibilities for failure? For example, one of the aforementioned anti-patterns would typically have one giant enumeration with each enumeration value representing a specific kind of failure, such as FILE_DOES_NOT_EXIST or NO_PERMISSIONS. Normally these are kept as general as possible so that they can be used across multiple, unrelated domains (such as networking components and file I/O components).

A design similar to this that one might consider for exceptions is to subclass one concrete exception type from std::exception for each type of failure or thing that might go wrong. So in my previous example, we would have the following:

namespace exceptions {
  class file_does_not_exist : public std::exception {};
  class no_permissions : public std::exception {};
}

I think this is closer to something that "feels better", but in the end this just seems like a maintenance nightmare, especially if you have hundreds of these "error codes" to translate over into classes.

Another approach I've seen is to simply use the standard <stdexcept> classes, such as std::runtime_error and have a string with the specifics. For example:

throw std::runtime_error( "file does not exist" );
throw std::runtime_error( "no permissions" );

This design is much more maintainable but makes it difficult or unfeasible to conditionally catch either of these exceptions should they both be potentially thrown from the same core location or function call.

So what would be a good, maintainable design for exception types? My requirements are simple. I'd like to have contextual information about what happened (did I run out of memory? Do I lack filesystem permissions? Did I fail to meet the preconditions of a function call (e.g. bad parameters)?), and I'd also like to be able to act on that information accordingly. Maybe I treat all of them the same, maybe I have specific catch statements for certain failures so I can recover from them differently.

My research on this has only lead me to this question: C++ exception class design

The user here asks a similar question that I am, and his/her code sample at the bottom is almost likable, but his/her base exception class does not follow the open/closed principle, so that wouldn't really work for me.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
void.pointer
  • 24,859
  • 31
  • 132
  • 243
  • 3
    What aspect of this question distinguishes it from the linked question besides a vague discomfort with the answers? – Adrian McCarthy Jun 02 '13 at 19:38
  • Possible duplicate of [C++ exception class design](https://stackoverflow.com/questions/1335561/c-exception-class-design) – TylerH Nov 20 '18 at 20:29

3 Answers3

30

The C++ standard library’s exception hierarchy is IMHO pretty arbitrary and meaningless. For example, it would probably just create problems if anyone started actually using e.g. std::logic_error instead of terminating when it’s clear that the program has a Very Nasty Bug™. For as the standard puts it,

“The distinguishing characteristic of logic errors is that they are due to errors in the internal logic of the program.”

Thus, at the point where it might otherwise seem reasonable to throw a std::logic_error the program state might be unpredictably fouled up, and continued execution might put the user’s data in harm’s way.

Still, like std::string the standard exception class hierarchy has a really really practically important and useful feature, namely that it’s formally standard.

So any custom exception class should be derived indirectly or (although I would not recommend it) directly from std::exception.

Generally, when the debates about custom exception classes raged ten years ago, I recommended deriving only from std::runtime_error, and I still recommend that. It is the standard exception class that supports custom messages (the others generally have hardcoded messages that one preferably should not change, since they have value in being recognizable). And one might argue that std::runtime_error is the standard exception class that represents recoverable failures (as opposed to unrecoverable logic errors, which can’t be fixed at run time), or as the standard puts it,

“runtime errors are due to events beyond the scope of the program. They cannot be easily predicted in advance”.

Sometimes the C++ exception mechanism is used for other things, treated as just a low-level dynamic destination jump mechanism. For example, clever code can use exceptions for propagating a successful result out of a chain of recursive calls. But exception-as-failure is the most common usage, and that’s what C++ exceptions are typically optimized for, so mostly it makes sense to use std::runtime_error as root for any custom exception class hierarchy – even if that forces someone who wants to be clever, to throw a “failure”-indicating exception to indicate success…

Worth noting: there are three standard subclasses of std::runtime_error, namely std::range_error, std::overflow_error and std::underflow_error, and that contrary to what their names indicate the latter two are not required to be be generated by floating point operations and are not in practice generated by floating point operations, but are AFAIK only generated by some – surprise! – std::bitset operations. Simply put, the standard library’s exception class hierarchy seems to me to have been thrown in there just for apperance’s sake, without any real good reasons or existing practice, and even without a does-it-make-sense check. But maybe I missed out on that and if so, then I still have something new to learn about this. :-)

So, std::runtime_error it is, then.

At the top of a hierarchy of custom exception classes, with C++03 it was useful to add in the important stuff missing from C++03 standard exceptions:

  • Virtual clone method (especially important for passing exceptions through C code).

  • Virtual throwSelf method (same main reason as for cloning).

  • Support for chained exception messages (standardizing a format).

  • Support for carrying a failure cause code (like e.g. Windows or Posix error code).

  • Support for getting a standard message from a carried failure cause code.

C++11 added support for much of this, but except for trying out the new support for failure cause codes and messages, and noting that unfortunately it’s pretty Unix-specific and not very suitable for Windows, I haven’t yet used it. Anyway, for completeness: instead of adding cloning and virtual rethrowing (which is the best that an ordinary application programmer can do in a custom exception class hierarchy, because as an application programmer you cannot hoist a current exception object out of the storage that the implementation’s exception propagation uses), the C++11 standard adds free functions std::current_exception() and std::rethrow_exception(), and instead of support for chained exception messages it adds a mixin class std::nested_exception and free functions std::rethrow_nested and std::rethrow_if_nested.

Given the partial C++11 support for the above bullet points, a new and modern custom exception class hierarchy should better integrate with the C++11 support instead of addressing the C++03 shortcomings. Well, except for the C++11 failure code thing, which seems to be very unsuitable for Windows programming. So, at the top of the custom hierarchy, right under std::runtime_error, there will ideally be at least one general exception class, and derived from that, one exception class that supports propagation of failure codes.

Now, finally, to the gist of the question: should one now best derive a unique exception class for every possible failure cause, or at least for major failure causes?

I say no: DON’T ADD NEEDLESS COMPLEXITY.

If or where it is can be useful for a caller to distinguish a certain failure cause, a distinct exception class for that is very useful. But in most cases the only information of interest to a caller is the single fact that an exception has occurred. It is very rare that different failure causes lead to different attempted fixes.

But what about failure cause codes?

Well, when that's what an underlying API gives you, it is just added work to create corresponding exception classes. But on the other hand, when you are communicating failure up in a call chain, and the caller might need to know the exact cause, then using a code for that means the caller will have to use some nested checking and dispatch inside the catch. So these are different situations: (A) your code is the original source of a failure indication, versus (B) your code uses e.g. a Windows or Posix API function that fails and that that indicates failure cause via a failure cause code.

Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • Excellent information, although I'm a bit confused about the cloning/selfthrow stuff you talked about. You said `you cannot hoist a current exception object out of the storage that the implementation’s exception propagation uses` which doesn't really make any sense to me. Can you explain or link me to some good reading material on this? – void.pointer Feb 22 '12 at 18:52
  • @RobertDailey: as an example, in C++11 the standard suggests in §18.8.5.6 that the implementation can use a `shared_ptr` or similar as `exception_ptr`. The pointed-at exception object must then be dynamically allocated. And for e.g. `current_exception()` to work, the internal storage of the currently-being-propagated exception must also be dynamically allocated, so that it can be referred to by the smart pointer result. But *you* don't know what kind of destruction is needed, so you can not implement your own `current_exception()`. And in C++03 it's worse. – Cheers and hth. - Alf Feb 23 '12 at 01:50
  • I've never used `exception_ptr` or `current_exception()`. I've always just done something like `throw MyException();`. Why would I use these two items instead? – void.pointer Mar 01 '12 at 17:07
  • @RobertDailey: mostly in order to propagate an exception through exception-unsafe code such as a C code. – Cheers and hth. - Alf Mar 01 '12 at 17:12
  • @Cheersandhth.-Alf I would just note that the C++11 `std::system_error`, `std::error_code`, `std::error_condition`, `std::error_category` and `std::errc` types provide the "missing failure code thing". Specifically, it provides the standard exception `system_error` that's constructed either from a portable `error_category` value and `error_condition` object, or from a platform-specific `error_code`. All are part of the new header ``, which was mainly required to support the new threading library. – Charles L Wilcox Apr 12 '13 at 15:35
  • @CharlesLWilcox: unfortunately, as noted in the answer, that functionality is pretty *nix-specific, in particular, producing Posix messages and relying on UTF-8 or similar `char`-based encoding for messages. it means that with C++11 the most *nix-specific and mostly-unusuable-in-Windows part of the language is no longer standard `main`. it was a shock to me. – Cheers and hth. - Alf Apr 12 '13 at 16:27
4

If the error condition is something that caller of your library could have prevented by changing logic of their code then derive your exception from logic_error. Generally, caller won't be able to do simple retry if logic_error is thrown. For example, someone is calling your code such that it would cause divide by 0 then you may create custom exception,

class divide_by_zero : public logic_error {
public:
    divide_by_zero(const string& message)
        : logic_error(message) { 
    }
}

If the error condition was something that could not have been prevented by the caller then derive from runtime_error. Some of these errors might be recoverable (i.e. caller can catch exception, re-try or ignore).

class network_down : public runtime_error {
public:
    network_down(const string& message)
        : runtime_error(message) { 
    }
}

This is also the general philosophy to design exceptions in standard library. You can view the exception code for GCC here.

Shital Shah
  • 63,284
  • 17
  • 238
  • 185
4

I have used boost::exception for a while now and I really like inserting arbitrary data into an exception. I do this in addition to specific exception types, e.g.

#define MY_THROW(x) \
    BOOST_THROW_EXCEPTION(x << errinfo_thread_id(boost::this_thread::get_id()))

class DatabaseException : public std::exception, public boost::exception { ... };

typedef boost::error_info< struct errinfo_message_, std::string > errinfo_message;

MY_THROW(DatabaseException(databaseHandle)
         << boost::errinfo_api_function("somefunction")
         << errinfo_message("somefunction failed terribly.")
         );

This way you can catch specific exceptions while also providing loads of detail from the throw site (e.g., file name, line number, thread id, ...).

It also provides some pretty printing of the exception message and its details. Most of the time I write that information in my log and abort the program, depending on the exception.

EDIT: As noted in the thread you cited, use shallow hierarchies. I use something like 3-4 exception classes that inherit directly from std::exception and boost::exception. I also put lots of details into the exceptions (e.g., the thread id).

Alex P.
  • 760
  • 5
  • 6
  • 1
    I tend to opt for an even flatter hieararchy now. Our project just uses `critical` and `recoverable` exceptions. The former basically indicates an non-recoverable condition and the best we can do is emit debugging information. The latter is of course recoverable and all code is expected to do so. It keeps it very simple. – edA-qa mort-ora-y Feb 22 '12 at 07:46
  • I can't really say I approve of the usage of a macro here, though. – void.pointer Mar 01 '12 at 17:31
  • 3
    @edA-qamort-ora-y: Is it really up to the throw site to decide whether an error is recoverable or not? – Markus Mayr Apr 15 '13 at 13:41
  • @MarkusMayr, yes, see my article http://mortoray.com/2013/01/03/how-to-handle-an-error/ – edA-qa mort-ora-y Apr 15 '13 at 13:44