I keep it simple.
A library has a base exception type extended from std:::runtime_error (that's from C++ apply as appropriate to other languages). This exception takes a message string so we can log; every throw point has a unique message (usually with a unique ID).
That's about it.
Note 1: In the situations where somebody catching the exception can fix the exceptions and re-start the action. I will add derived exceptions for things that can be potentially uniquely be fixed at a remote location. But this is very very rare (Remember the catcher is unlikely to be close to the throw point thus fixing the problem is going to be hard (but everything is dependent on situation)).
Note 2: Sometimes the library is so simple it is not worth giving it its own exception and std::runtime_error will do. It is only important to have an exception if the ability to distinguish it from std::runtime_error can give the user enough information to do something with it.
Note 3: Within a class I usually prefer error codes (but these will never escape across the public API of my class).
Looking at your trade offs:
The trade-offs I see include:
More exception classes can allow very fine grain levels of error handling for API users (prone to user configuration or data errors, or files not being found)
Do more exceptions really give you finer grain control? The question becomes can the catching code really fix the error based on the exception. I am sure there are situations like that and in these cases you should have another exception. But all the exceptions you have listed above the only useful correction is to generate a big warning and stop the application.
More exception classes allows error specific information to be embedded in the exception, rather than just a string message or error code
This is great reason for using exceptions. But the information must be useful to the person who is caching it. Can they use the information to perform some corrective action? If the object is internal to your library and can not be used to influence any of the API then the information is useless. You need to be very specific that the information thrown has a useful value to the person that can catch it. The person catching it is usually outside your public API so tailor your information so that it can be used with things in your public API.
If all they can do is log the exception then it is best to just throw an error message rather than lots of data. As the catcher will usually build an error message with the data. If you build the error message then it will be consistent across all catchers, if you allow the catcher to build the error message you could get the same error reported differently depending on who is calling and catching.
Less exceptions, but embedding an error code that can be used as a lookup
You have to determine weather the error code can be used meaningfully. If it can then you should have its own exception. Otherwise your users now need to implement switch statements inside there catch (which defeats the whole point of having catch automatically handle stuff).
If it can't then why not use an error message in the exception (no need to split the code and the message it makes it a pain to look up).
Returning error codes and flags directly from functions (sometimes not possible from threads)
Returning error codes is great internally. It allows you to fix bugs there and then and you have to make sure you fix all error codes and account for them. But leaking them across your public API is a bad idea. The problem is that programmers often forget to check for error states (at least with an exception an unchecked error will force the application to quit an un-handled error will generally corrupt all your data).
Implemented an event or callback system upon error (avoids stack unwinding)
This method is often used in conjunction with other error handling mechanism (not as an alternative). Think of your windows program. A user initiates an action by selecting a menu item. This generates an action on the event queue. The event queue eventually assigns a thread to handle the action. The thread is supposed to handle the action and eventually return to the thread pool and await another task. Here an exception must be caught at the base by the thread tasked with the job. The result of catching the exception will usually result in an event being generated for the main loop which will eventually result in an error message being displayed to the user.
But unless you can continue in the face of the exception the stack is going to unwind (for the thread at least).
All exceptions should inherit from std::exception
.
Suppose, for example, I need to call ComplexOperationThatCouldFailABunchOfWays()
, and I want to handle any exceptions that it could throw. If everything inherits from std::exception
, this is easy. I only need a single catch
block, and I have a standard interface (what()
) for getting details.
try {
ComplexOperationThatCouldFailABunchOfWays();
} catch (std::exception& e) {
cerr << e.what() << endl;
}
If exceptions do NOT inherit from std::exception
, this gets much uglier:
try {
ComplexOperationThatCouldFailABunchOfWays();
} catch (std::exception& e) {
cerr << e.what() << endl;
} catch (Exception& e) {
cerr << e.Message << endl;
} catch (framework_exception& e) {
cerr << e.Details() << endl;
}
Regarding whether to throw runtime_error
or invalid_argument
versus creating your own std::exception
subclasses to throw: My rule of thumb is to introduce a new subclass whenever I need to handle a particular type of error differently than other errors (i.e., whenever I need a separate catch
block).
- If I introduce a new exception subclass for every conceivable type of error, even if I don't need to handle them separately, then that adds a lot of class proliferation.
- If I reuse existing subclasses to mean something specific (i.e., if a
runtime_error
thrown here means something different than a generic runtime error), then I run the risk of conflicting with other uses of the existing subclass.
- If I don't need to handle an error specifically, and if the error that I'm throwing exactly matches one of the existing standard library's errors (such as
invalid_argument
), then I reuse the existing class. I just don't see much benefit to adding a new class in this case. (The C++ Core Guidelines disagree with me here - they recommend always using your own classes.)
The C++ Core Guidelines have further discussion and examples.
Best Answer
First, I feel obliged to point out that
std::exception
and its children were designed a long time ago. There are a number of parts that would probably (almost certainly) be different if they were being designed today.Don't get me wrong: there are parts of the design that have worked out pretty well, and are pretty good examples of how to design an exception hierarchy for C++ (e.g., the fact that, unlike most other classes, they all share a common root).
Looking specifically at
logic_error
, we have a bit of a conundrum. On one hand, if you have any reasonable choice in the matter, the advice you quoted is right: it's generally best to fail as fast and noisily as possible so it can be debugged and corrected.For better or worse, however, it's hard to define the standard library around what you should generally do. If it defined these to exit the program (e.g., calling
abort()
) when given incorrect input, that would be what always happened for that circumstance--and there are actually quite a few circumstances under which this probably isn't really the right thing to do, at least in deployed code.That would apply in code with (at least soft) real-time requirements, and minimal penalty for an incorrect output. For example, consider a chat program. If it's decoding some voice data, and gets some incorrect input, chances are a user will be a lot happier to live with a millisecond of static in the output than a program that just shuts down completely. Likewise when doing video playback, it may be more acceptable to live with producing the wrong values for some pixels for a frame or two than have the program summarily exit because the input stream got corrupted.
As for whether to use exceptions to report certain types of errors: you're right--the same operation might qualify as an exception or not, depending on how it's being used.
On the other hand, you're also wrong--using the standard library doesn't (necessarily) force that decision on you. In the case of opening a file, you'd normally be using an iostream. Iostreams aren't exactly the latest and greatest design either, but in this case they get things right: they let you set an error mode, so you can control whether failing to open a file with result in an exception being thrown or not. So, if you have a file that's really necessary for your application, and failing to open it means you have to take some serious remedial action, then you can have it throw an exception if it fails to open that file. For most files, that you'll try to open, if they don't exist or aren't accessible, they'll just fail (this is the default).
As for how you decide: I don't think there is an easy answer. For better or worse, "exceptional circumstances" isn't always easy to measure. While there are certainly cases that are easy to decide must be [un]exceptional, there are (and probably always will be) cases where it's open to question, or requires knowledge of context that's outside the domain of the function at hand. For cases like that, it may at least be worth considering a design roughly similar to this part of iostreams, where the user can decide whether failure results in an exception being thrown or not. Alternatively, it's entirely possible to have two separate sets of functions (or classes, etc.), one of which will throw exceptions to indicate failure, the other of which uses other means. If you go that route, chances are pretty good that one should be a wrapper around the other, or both act as wrappers around a shared set of functions that implement the real guts of the work.