11

The basics: what I know

I understand the basics of exceptions:

  • If a function cannot perform its assigned task, it can signal it by throwing an exception,
  • When writing a function, one must make sure it is exception-safe, i.e. keep in mind what calls can throw an exception and make sure that the function behaves properly if they do (providing the basic, strong or nothrow guarantee),
  • If relevant, write a try/catch block at some level to handle the exception instead of it propagating until it terminates the application.
  • If relevant, write a try/catch block to translate an exception and rethrow it
  • Compared to return codes, exceptions have 3 main advantages
    1. They cannot be ignored when an error occurs
    2. You can test for the success of several calls with a single test
    3. They avoid bloating the code by propagating if not caught: only functions detecting and handling errors need to manipulate exceptions, and all the others functions can be written as if every call always succeeded (though exception-safety still needs to be kept in mind)

So usually you would have a low-level function that detect the error and throws:

void doXXX()
{
    bool const condition = ...;
    if (!condition)
    {
        throw std::runtime_error("Cannot do XXX because condition not met");
    }
// Do XXX

}

And a high-level function that catches the error and handles it:

void doYYY()
{
    try
    {
        // Call various functions that themselves call functions that call...
        // and at some point one of them calls doXXX()
        doZZZ();
    }
    catch (std::exception const& e)
    {
        logger() << e.what() << endlog();
        // Do something
    }
}

The problem: what I want some guidance on

Now if I want to use that in practice in a project, I need to know 3 things:

  • When I need to throw an exception, what kind of exception do I throw?
  • Between the point where I throw the exception and the point where I catch and handle it, when do I have to catch & rethrow it ?
  • When I do, what kind of exception do I throw ?

What I've already thought about and why it's not satisfying

A "simple" approach:

  • Assuming I only care about logging, maybe I can always throw std::runtime_error with a string that says why the function could not perform its task
  • Context from higher-level functions is often necessary to know what's going on (e.g. if you get a syntax error when parsing several configuration files, you want to know in which file it occured)
  • In the functions that have information that needs to be logged, catch & rethrow using std::throw_with_nested(std::runtime_error("some context information"))
  • Since C++ exceptions do not contain a backtrace, it's also useful to catch & rethrow to get an idea how we got to the exception being throw (especially if it's thrown by a function used everywhere within the code)
  • This works decently, but sometimes it leads to basically catching and rethrowing in every function
  • This is bad because it defeats the point of propagating exceptions to avoid bloating the code
  • It's also directly against best practices. The C++ Core Guidelines explicitly say to avoid catching everything in every function and to minimize the use of explicit try/catch

Going further:

  • I should not care about logging only.
  • The C++ Core Guidelines say to use purpose-designed user-defined types as exceptions
    • Such exceptions can be used to distinguish various types of errors when needed (catching some exceptions and not others)
    • They are also semantically richer (they could allow for example to display an error message for the user in various languages, which the string within the exception is insufficient to do)
  • Item 11 of Herb Sutter's Exceptional C++ Style book also suggests to catch low-level problems and translate them into higher-level semantics, using the example:
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. ...
}
  • Taken together, it kind of feels like what I'm supposed to do is:
    • for each function foo() that can throw
    • create a custom exception FooException (presumably derived from std::runtime_error or std::logic_error, depending on what makes sense)
    • catch all exceptions in foo()
    • add dedicated handlig for some exception types if relevant
    • in all cases, call std::throw_with_nested(FooException{someContext});
  • The problem is that I'm back to catching and rethrowing in every function, which as said before goes against best practices.

I feel that I am missing something. I'm probably making wrong assumptions that make it seem like the best practices contradict each other when I know it's unlikely.

Edit: Maybe this is all a bit too abstract, let's reframe this with an example:

  • Imagine a user has clicked on a Load Parameters button and selected several files
  • Some code tries to extract information from the files, but at some point it detects that something is wrong with the syntax and throws an exception
  • That exception propagates and at some point you want to handle that error by returning to a pre-load state, showing the user a message telling them what the issue was so that they can fix the files and click on the button again to retry
  • How would you bring all the relevant information to the catch(), including in what part of what file the issue was?
  • How would you log what happened?
  • More generally, when you write code that can throw, how do you make sure the person calling that code has all the information they need within the exception to handle the error properly?
Eternal
  • 235
  • 3

3 Answers3

16

You are going overboard. One of the main advantages of exceptions is skipping over all the functions between where the error occurs, and where it can be fixed. If you wrap every function in try ... catch ... then you lose that benefit.

The default action for a function that contains calls that can throw, is to ensure the strong (or basic if strong is impossible) exception guarantee, and do nothing with the exceptions. Let them flow through.

If you can do something meaningful with an exception, only then do you catch it. That can mean, at boundaries between a low-level module and a high-level module, you wrap (some!) low-level exceptions in a high-level wrapper where that grouping is helpful the higher level.

From your example: Session::Open, perhaps some errors indicate transient network issues, so you catch those and retry, whereas other errors indicate a configuration issue. Because the consumers of this code want to distinguish between failing to connect and other issues, exceptions that would pass through are wrapped in Session::OpenFailed.

When I need to throw an exception, what kind of exception do I throw?

One with information that callers need to handle the exception.

If your caller wants to distinguish e.g. "206 Partial Content", "400 Bad Request", and "401 Unauthorised", then you probably should include the http error code. If they are just going to exit gracefully, then you don't.

Between the point where I throw the exception and the point where I catch and handle it, when do I have to catch & rethrow it ?

Catch and rethrow? Nowhere. Catch, add in extra context, throw nested exception: places where that extra context is needed by the caller.

Batch file processing from a UI has often extended requirements for returning error information, and it is clear that you can solve this by using a custom exception: it would contain a vector of exceptions thrown by the files in the batch. So you may have 3 layers: a lowest layer which catches the exception from processing a single file, an intermediate layer which collects those exceptions and bundles them in a single exception, and an outer layer which catches the exception, and displays the results to the user, but this is far from "catching and rethrowing in every function", so long as your functions also follow the advice to have short, focused functions.

When I do, what kind of exception do I throw ?

One with the extra context. See above.

This context isn't the what() of std::exception, it the type, and that type's data members. But you don't need excruciating detail for it's own sake. You only need detail when the handling is using that detail to make decisions.

N.b. while it is true that std::exception doesn't have a stack trace, there is now std::basic_stacktrace, which you can include in your own exception type.

Caleth
  • 12,190
  • 2
    Let me give you a concrete example I encountered in which that was not enough. I was reading a configuration file using Boost Json. The various fields were parsed into boost::json::object, each with entries such as "name" and "value". Except a field was missing its "value" entry: the call to field.at("value") throws a std::invalid_argument, I catch it all the way back to main() and it says that something like Json Object has no entry named "value". To know which field is missing its value (or other things like in what file), you need to catch and rethrow to add context – Eternal Jun 30 '25 at 13:19
  • I know I'm not supposed to wrap every function in try/catch. I say so myself in my question. The problem is that I know I need to 1) add some context to the exceptions (cf. my other comment), 2) throw my own exception types to allow users to discriminate (according to core guidelines), and 3) make sure not to expose low-level exceptions but to translate them into higher-level ones like in the example I pulled from Sutter's book... and I don't know how to do all that without wrapping every function in try/catch – Eternal Jun 30 '25 at 13:28
  • @Eternal that sounds like you want a debugger that breaks on the throw, not the catch. – Caleth Jun 30 '25 at 13:45
  • 2
    Or use a better library, e.g. nlohmann, which will tell you where the error was in the input. – Caleth Jun 30 '25 at 13:52
  • 2
    @Caleth Imagine a user has clicked on a "load parameters" button and selected several files. Some code tries to extract information from the files, but at some point it detects that something is wrong with the syntax and throws an exception. That exception propagates and at some point you want to handle that error by returning to a pre-load state, showing the user a message telling them what the issue was so that they can fix the files and click on the button again to retry. How would you bring all the relevant information to the catch(), including in what part of what file the issue was? – Eternal Jun 30 '25 at 14:04
  • @DocBrown There isn't a pre-defined set of exception types that covers all situations. E.g. the exceptions defined in the standard library are applicable to the functions in the standard library. They are really generic sounding because std is really generic. But in some situations that's all you need. – Caleth Jun 30 '25 at 14:32
  • @DocBrown the design aspect seems to be covered - thrown exceptions are part of interface/contract of a component. The contract is designed for consumers/clients, therefore the type and contents of an exception should be derived from client's needs. – Basilevs Jun 30 '25 at 14:37
  • @Basilevs: yes, and I think a good answer should tell readers what this exactly means (ideally by an example), and why it does not lead to catch/rethrow at every level. – Doc Brown Jun 30 '25 at 14:43
  • I think your edits have clearly improved the post. But what do you mean by "Catch and rethrow? Nowhere."? Formerly, you wrote "at boundaries between a low-level module and a high-level module, you wrap (some!) low-level exceptions" - but doesn't that exactly mean "catch the low-level exceptions, put them into a higher-level exception and rethrow them"? Sounds contradictory to me. – Doc Brown Jun 30 '25 at 15:55
  • @DocBrown throw a new exception, that probably has the inner exception as a member. Not re-throw the inner exception. – Caleth Jun 30 '25 at 23:37
8

Personally, I'd say you're coming at this from completely the wrong direction.
Stop thinking about throwing Exceptions for a minute and consider what, if anything, you can do if you catch one!

If the answer is "nothing", then there's no point using Exceptions. Period.

The most important part of Structured Exception Handling is that last part - "Handling".
"Handling" an Exception means doing something useful about it so that, to whatever code called you, it's as if the Exception never happened.

void A() { 
   B(); 
}

void B() { try { C(); } catch() { /* ... handle the exception *usefully / } }

The method A() does not see any Exceptions, no matter what happens in C() - and that's a well-handled Exception.

So, coming back to your three questions:

When I need to throw an exception, what kind of exception do I throw?

Whatever kind you can usefully catch and do something about.
If you can't do anything useful with an Exception, don't catch it and just let it propagate upwards and outwards to something that can.

Between the point where I throw the exception and the point where I catch and handle it, when do I have to catch & rethrow it?

Almost NEVER.
The whole point of SEH is that you put handlers (catches) where they can do something useful. Let the Exception infrastructure worry about where they are.
Also, in early versions of .Net, Exceptions were effectively thrown twice; once to locate the handler (catch) block and collect any finally blocks on the way and then again to unwind the call stack, executing each of the finally blocks on the way before passing control to the selected handler. That made throwing Exceptions [a lot] painfully slow.

Many of us have seen often-generated, "boiler-plate" code that catches and re-throws Exceptions in every method. Yuck! Such code (a) abuses Structured Exception Handling to the point that it might as well return error codes from each and every method and (b) it may well well run appallingly slowly.

When I do, what kind of exception do I throw?

If you're building a library that will be shipped to clients, you might want to catch Exceptions at the process boundary and rethrow a new Exception containing only selected details and the StackTrace at that point of entry, thereby "hiding" the internal implementation of your library. Within your library, though, just let Exceptions propagate by themselves.

Phill W.
  • 13,093
  • 2
    Imagine a user has clicked on a "load parameters" button and selected several files. Some code tries to extract information from the files, but at some point it detects that something is wrong with the syntax and throws an exception. That exception propagates and at some point you want to handle that error by returning to a pre-load state, showing the user a message telling them what the issue was so that they can fix the files and click on the button again to retry. How would you bring all the relevant information to the catch(), including in what part of what file the issue was? – Eternal Jun 30 '25 at 15:18
  • Completely ignores context enrichment aspect. Good point about consumer- orientation though. – Basilevs Jun 30 '25 at 15:22
-1

Caveat: My experience is primarily with Exceptions in JVM languages, I think the concepts are transferable but there may be implementation details I am unaware of.

I agree with the first concept (focussing on how exceptions are handled) in Phil W's answer, but disagree with how that gets flushed out.

Between the point where I throw the exception and the point where I catch and handle it, when do I have to catch & rethrow it ?

The case where I would consider try, catch and throw is where I only wanted to partially handle the exception / some cleanup not automatically handled by another mechanism.

In most other cases, you are adding a try/catch because there is a particular goal (or goals) you are trying to achieve:

  • Log and/or record statistics that a problem occurred.
  • Retry an operation for example try (again) to open a network connection.
    • There may be a timeout or counter to prevent infinite retries.
  • Translate and/or route a meaningful message/popup to the user so they know a problem occurred.
  • Prevent propagation of the exception into a thread pool i.e. keep a thread alive and return it to the pool.
  • Translate the error into another form:
    • HTTP Status code / message body for a REST request.
    • Regular status/error code.
    • A different exception - typically a higher level exception, for example "Document could not be opened"

When you throw a different exception (the final case above) the critical observation is that the new exception will also be need to be handled and you have the same choices for handling it, as you did for the original exception.

Exceptions carry information in three ways:

  • The type / type hierarchy of the exception.
  • Any fields that the exception contains (including causing/nested exceptions)
  • The location that the exception was thrown from, some exceptions contain a call stack however part of that stack can be implied from the location where the try/catch is located.

Interrogating the exception to find the location the exceptions was thrown from is generally consider bad practice, as most programmers do not expect exception handlers to do that / won't consider it when refactoring.

Therefore higher level exception handlers have less information than low level exception handlers - taken to the extreme a try/catch in the main function simply knows that an exception occurred "somewhere" in the code.

Therefore when you morph (throw a higher level exception) you are updating the way the exception contains it's information (it's no longer possible to identify the exact location the exception came from), so if higher level logic requires this, it will have to rely on the type of the exception or any fields that you choose to include.

When I need to throw an exception, what kind of exception do I throw?

This thought process also applies to the first time you throw an exception, say you choose to throw a FileNotFoundException, that may be acceptable if you know the higher level logic will only have to deal with one case where a file is loaded. If more than one file is loaded, additional context may be needed either when the first exception is thrown or as part of a higher level mapping to a different exception.

DavidT
  • 4,601
  • 1
    The first part of your answer about try-finally can be confusing, because this question is specifically about C++ and standard C++ has no try-finally. – pschill Jun 30 '25 at 20:13
  • 3
    In C++ resources are cleaned up in destructors, no matter how a scope is left, so there is no finally – Caleth Jun 30 '25 at 23:32