std::throw_with_nested
and std::rethrow_if_nested
.Within our catch
blocks, we do not necessarily have to fully recover from the exception. We can create some code that ultimately (or conditionally) rethrows the exception. This lets us defer error handling to the calling code, elsewhere in the stack.
To rethrow an exception, we just use the throw
statement within our catch
statement:
To rethrow an exception, we just use the throw
statement within our catch
statement:
#include <iostream>
#include <exception>
using namespace std;
int Divide(int x, int y) {
if (y == 0) throw logic_error {
"Cannot divide by zero"
};
return x/y;
}
void CallFunction(int x, int y) {
try {
Divide(x, y);
} catch (invalid_argument& e) {
cout << "I have caught the exception" << endl;
throw;
}
}
int main() {
try {
CallFunction(2, 0);
} catch (invalid_argument& e) {
cout << "I have caught it too" << endl;
}
cout << "Goodbye";
}
I have caught the exception
I have caught it too
Goodbye
There is a common mistake when rethrowing exceptions. Rather than simply using the expression throw
in isolation, we may be tempted to use it with the original error:
#include <iostream>
void func() {
try {
throw std::invalid_argument {
"Useful Error Message"
};
} catch (std::exception& e) {
std::cout << "First Catch: "
<< e.what() << std::endl;
throw e;
}
}
int main() {
try {
func();
} catch (std::exception& e) {
std::cout << "Second Catch: "
<< e.what() << std::endl;
}
}
First Catch: Useful Error Message
Second Catch: std::exception
Note the first catch statement has access to our custom message but, after rethrowing it, that information is lost.
This has the effect of throwing a copy of the exception, which is not desirable. Creating a copy unnecessarily has a performance impact, but worse, it can unintentionally change the type and lose information from the original.
That is exactly what is happening in the previous example. Our original exception had a type of std::invalid_argument
. However, in our first catch
statement, we are working with a base type -the more generic std::exception
Once we rethrow that, our second catch is now working with a simple std::exception
, which doesn’t support custom error messages.
Unless we explicitly want to change the type of error that is thrown, we should simply use throw
in isolation from within our catch
statement, without being explicit about what it should throw:
#include <iostream>
void func() {
try {
throw std::invalid_argument {
"Useful Error Message"
};
} catch (std::exception& e) {
std::cout << "First Catch: "
<< e.what() << std::endl;
throw;
}
}
int main() {
try {
func();
} catch (std::exception& e) {
std::cout << "Second Catch: "
<< e.what() << std::endl;
}
}
First Catch: Useful Error Message
Second Catch: Useful Error Message
Converting an instance of a derived class to a base class in this way is referred to as slicing. This is often a mistake - any data that the object had that does not exist in the base class is lost in this operation.
Whilst handling an exception within a catch
block, there may be scenarios where yet another exception can occur, thus leaving us with two exceptions being active at the same time.
For this scenario, C++ has the concept of a nested exception. We throw the new exception, but include the previous exception within. So, the previous exception becomes nested in the new exception.
To do this, we use the std::throw_with_nested
function, and we pass our new exception:
void ThrowNestedException() {
try {
throw std::runtime_error{"First Exception"};
} catch (std::runtime_error& e) {
std::throw_with_nested(
std::logic_error{"Second Exception"}
);
}
}
We now have a std::logic_error
, with a std::runtime_error
nested within it.
We can catch the logic error in the usual way:
try {
ThrowNestedException();
} catch (std::logic_error& e) {
std::cout << "Caught a logic error: "
<< e.what();
}
Caught a logic error: Second Exception
When we used throw_with_nested
, our original exception used multi-inheritance to gain a new type. In addition to its original type (a std::logic_error
, in this case), our exception is now also a std::nested_exception
.
To check if an exception is a std::exception
, we can dynamic_cast
it in the usual way:
auto nested {
dynamic_cast<std::nested_exception*>(&e)
};
As with any dynamic_cast
, if the argument was a std::nested_exception
, a pointer is returned. If it wasn’t, a nullptr
is returned.
So, we can use an if
statement to check if our exception really was a nested exception:
try {
ThrowNestedException();
} catch (std::logic_error& e) {
std::cout << "Caught a logic error: "
<< e.what();
auto nested {
dynamic_cast<std::nested_exception*>(&e)
};
if (nested) {
std::cout << "\nThere is a nested exception!\n";
}
}
Caught a logic error: Second Exception
There is a nested exception!
Once we’ve dealt with the new exception, we can get back to dealing with the original issue. In this example, that was the std::runtime_error
The standard way of doing this is to rethrow the nested exception. We can do this from the nested_exception*
returned from the dynamic cast, by calling rethrow_nested
:
try {
ThrowNestedException();
} catch (std::logic_error& e) {
std::cout << "Caught a logic error: "
<< e.what();
auto nested{
dynamic_cast<std::nested_exception*>(&e)
};
if (nested) {
std::cout << "\nThere is a nested exception!\n";
try {
nested->rethrow_nested();
} catch (std::runtime_error& e) {
std::cout << "Caught a runtime error: "
<< e.what();
}
}
}
Caught a logic error: Second Exception
There is a nested exception!
Caught a runtime error: First Exception
This dynamic cast and rethrow pattern is so common that the standard library provides a method for us to make our code a little simpler. We can just pass our outer exception to the std::rethrow_if_nested
function:
try {
ThrowNestedException();
} catch (std::logic_error& e) {
std::cout << "Caught a logic error: "
<< e.what();
try {
std::rethrow_if_nested(e);
} catch (std::runtime_error& e) {
std::cout << "\nThere is a nested exception!";
<< "\nCaught a runtime error: "
<< e.what();
}
}
Caught a logic error: Second Exception
There is a nested exception!
Caught a runtime error: First Exception
In these simplified examples, we are throwing and catching the error immediately. But, as we covered in our previous lesson covering how exceptions interact with the call stack, this need not be the case.
We don’t need to wrap our rethrow in a nested try
statement - we can just throw our error, and let it be caught anywhere else on the call stack.
try {
ThrowNestedException();
} catch (std::logic_error& e) {
std::cout << "Caught a logic error: "
<< e.what();
std::rethrow_if_nested(e);
}
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.