Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Propagating Exceptions Up the Call Stack
Learn to rethrow exceptions for robust error handling in programs.
Rethrowing exceptions
Occasionally you'll encounter situations where you want to catch an exception but cannot (or should not) fully handle it at the point where you catch it. This commonly occurs when you want to log an error but pass the issue along to the caller for actual handling.
When a function uses return codes, this is straightforward. Consider this example:
Connection* establishConnection(std::string_view hostname)
{
Connection* conn{};
try
{
conn = new Connection{};
conn->connect(hostname); // assume this throws std::string on failure
return conn;
}
catch (const std::string& error)
{
// Connection establishment failed
delete conn;
// Write error to log file
g_log.writeError("Failed to connect to host");
}
return nullptr;
}
In this code, the function attempts to create a Connection object, establish the connection, and return the Connection object. When something goes wrong (perhaps the hostname is invalid), the exception handler logs an error and reasonably returns a null pointer.
Now consider this function:
int fetchUserID(Connection* conn, std::string_view tableName, std::string_view username)
{
assert(conn);
try
{
return conn->getIntValue(tableName, username); // throws std::string on failure
}
catch (const std::string& error)
{
// Write error to log file
g_log.writeError("Failed to fetch user ID");
// But we haven't actually handled this error
// What do we do here?
}
}
When this function succeeds, it returns an integer value—any integer could be valid.
But what about the case where something goes wrong with getIntValue()? In that case, getIntValue() throws a string exception, which is caught by the catch block in fetchUserID(), which logs the error. But how do we tell the caller of fetchUserID() that something went wrong? Unlike the previous example, there's no obvious return code we can use (since any integer return value could be valid).
Throwing a new exception
One straightforward solution is throwing a new exception:
int fetchUserID(Connection* conn, std::string_view tableName, std::string_view username)
{
assert(conn);
try
{
return conn->getIntValue(tableName, username); // throws std::string on failure
}
catch (const std::string& error)
{
// Write error to log file
g_log.writeError("Failed to fetch user ID");
// Throw different exception type up the stack for caller to handle
throw -1;
}
}
In this example, the program catches the string exception from getIntValue(), logs the error, and then throws a new exception with int value -1. Although it may seem unusual to throw an exception from a catch block, this is allowed. Remember, only exceptions thrown within a try block are eligible for that try block's catch handlers. An exception thrown within a catch block won't be caught by that catch block. Instead, it propagates up the stack to the caller.
The exception thrown from a catch block can be any type—it doesn't need to match the type of the exception that was just caught.
Throwing a new exception from a catch block is useful when you want to translate a low-level exception (like a database error) into a higher-level exception (like "user not found") that makes more sense to the caller.
Rethrowing an exception (the wrong way)
Another option is rethrowing the same exception. One way to do this is:
int fetchUserID(Connection* conn, std::string_view tableName, std::string_view username)
{
assert(conn);
try
{
return conn->getIntValue(tableName, username); // throws std::string on failure
}
catch (const std::string& error)
{
// Write error to log file
g_log.writeError("Failed to fetch user ID");
throw error;
}
}
Although this works, this method has downsides. First, it doesn't throw the exact same exception that was caught—rather, it throws a copy-initialized copy of variable error. Although the compiler may elide the copy, it might not, so this could be less performant.
More significantly, consider what happens in this case:
int fetchUserID(Connection* conn, std::string_view tableName, std::string_view username)
{
assert(conn);
try
{
return conn->getIntValue(tableName, username); // throws TimeoutError on failure
}
catch (NetworkError& error)
{
// Write error to log file
g_log.writeError("Failed to fetch user ID");
throw error; // Danger: this throws a NetworkError object, not a TimeoutError object
}
}
In this case, getIntValue() throws a TimeoutError object, but the catch block catches a NetworkError reference. This is fine since we can have a NetworkError reference to a TimeoutError object. However, when we throw an exception, the thrown exception is copy-initialized from variable error. Variable error has type NetworkError, so the copy-initialized exception also has type NetworkError (not TimeoutError!). In other words, our TimeoutError object has been sliced!
You can see this in the following program:
#include <iostream>
class NetworkError
{
public:
NetworkError() {}
virtual void print() { std::cout << "NetworkError"; }
};
class TimeoutError : public NetworkError
{
public:
TimeoutError() {}
void print() override { std::cout << "TimeoutError"; }
};
int main()
{
try
{
try
{
throw TimeoutError{};
}
catch (NetworkError& error)
{
std::cout << "Caught NetworkError, which is actually a ";
error.print();
std::cout << '\n';
throw error; // the TimeoutError object gets sliced here
}
}
catch (NetworkError& error)
{
std::cout << "Caught NetworkError, which is actually a ";
error.print();
std::cout << '\n';
}
return 0;
}
This prints:
Caught NetworkError, which is actually a TimeoutError Caught NetworkError, which is actually a NetworkError
The second line indicates that NetworkError is actually a NetworkError rather than a TimeoutError, proving the TimeoutError object was sliced.
Using
throw error; instead of throw; causes object slicing when catching a base class reference to a derived exception. The derived exception information is lost, which can make debugging much harder.
Rethrowing an exception (the right way)
Fortunately, C++ provides a way to rethrow the exact same exception that was just caught. To do so, simply use the throw keyword from within the catch block with no associated variable:
#include <iostream>
class NetworkError
{
public:
NetworkError() {}
virtual void print() { std::cout << "NetworkError"; }
};
class TimeoutError : public NetworkError
{
public:
TimeoutError() {}
void print() override { std::cout << "TimeoutError"; }
};
int main()
{
try
{
try
{
throw TimeoutError{};
}
catch (NetworkError& error)
{
std::cout << "Caught NetworkError, which is actually a ";
error.print();
std::cout << '\n';
throw; // note: rethrowing the object here
}
}
catch (NetworkError& error)
{
std::cout << "Caught NetworkError, which is actually a ";
error.print();
std::cout << '\n';
}
return 0;
}
This prints:
Caught NetworkError, which is actually a TimeoutError Caught NetworkError, which is actually a TimeoutError
This throw keyword that doesn't specify anything rethrows the exact same exception that was just caught. No copies are made, so we don't have to worry about performance-killing copies or slicing.
When rethrowing an exception is required, this method should be preferred over the alternatives.
When rethrowing the same exception, use the throw keyword by itself (just
throw;). This avoids copying and prevents slicing.
When to rethrow vs handle
| Situation | Action |
|---|---|
| You can fully resolve the error | Handle it - don't rethrow |
| You need to log but can't fix | Rethrow with throw; |
| You need to add context | Throw a new exception with wrapped info |
| You need to translate exception type | Throw a new exception of the appropriate type |
| You need cleanup before propagating | Do cleanup, then rethrow with throw; |
Rethrow when you need to do something in response to an exception (logging, cleanup, resource release) but the actual recovery needs to happen somewhere else. Handle completely when you can actually fix the problem at this level.
Summary
Partial exception handling: Sometimes you want to catch an exception to perform logging or cleanup but cannot fully handle it. In these cases, you need to pass the exception along to the caller for actual handling.
Throwing a new exception: One solution is catching an exception, performing necessary operations (like logging), then throwing a different exception type. The exception thrown from a catch block can be any type and propagates up the stack to the caller.
Rethrowing the same exception: To rethrow the exact same exception that was just caught, use the throw keyword by itself (without specifying an exception object).
The problem with throw error: Using throw error (where error is the caught exception) creates a copy of the exception object. More importantly, if you caught a base class reference to a derived exception object, throw error will slice the object, throwing only the base class portion.
Best practice for rethrowing: Always use throw; (without an exception variable) when rethrowing. This rethrows the exact same exception object without copying and without slicing, ensuring both correctness and performance.
Rethrowing exceptions allows functions to perform intermediate processing (like logging or cleanup) while delegating the actual error handling responsibility to callers higher up the call stack.
Propagating Exceptions Up the Call Stack - Quiz
Test your understanding of the lesson.
Practice Exercises
Rethrowing Exceptions
Create a logging function that catches exceptions, logs them, and rethrows them for the caller to handle. Demonstrate the difference between rethrowing with 'throw;' versus 'throw error;'.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!