A common scenario in exception handling is the need to store and transfer exceptions to other functions, and potentially rethrow those exceptions.
In this lesson, we delve into the nuances of rethrowing exceptions. We'll explore various scenarios, including conditional rethrowing, preventing data loss during rethrowing, and advanced techniques using std::exception_ptr
.
To rethrow an exception, we just use the throw
statement within our catch
statement:
try {
//...
} catch(...) {
throw;
}
One use case of rethrowing exceptions allows functions to "observe" exceptions flowing through them, without necessarily handling them. Below, our Login()
function checks for AuthenticationError
exceptions and generates some logging when they happen.
But even though Login()
catches the exceptions, it doesn’t handle them. It just rethrows it to be handled elsewhere in the stack. In this example, it is the main
function that ultimately handles the exception:
#include <iostream>
using std::string;
class AuthenticationError{/*...*/}
void Auth(string Email, string Password) {
throw AuthenticationError{Email, Password};
}
void Login(string Email, string Password) {
try {
Auth(Email, Password);
} catch (AuthenticationError& e) {
std::cout << "Security Alert - Login Fail: "
<< e.Email << '\n';
throw;
}
}
int main() {
try {
Login("test@example.com", "wrong");
} catch (std::exception& e) {
std::cout << "Handled by main()";
}
}
Security Alert - Login Fail: test@example.com
Handled by main()
The second main way we can use this is to conditionally rethrow. This allows us to write a catch
block that is capable of handling some errors, whilst the rest are sent up the stack.
Below, our Login()
function catches AuthenticationError
exceptions again. This time, if the Password
field is empty, the Login()
function takes care of that locally. Otherwise, it rethrows it for main
to handle:
#include <iostream>
using std::string;
class AuthenticationError{/*...*/}
void Auth(string Email, string Password) {
throw AuthenticationError{Email, Password};
}
void Login(string Email, string Password) {
try {
Auth(Email, Password);
} catch (AuthenticationError& e) {
if (Password.empty()) {
std::cout << "Handled by Login()";
} else {
throw;
}
}
}
int main() {
try {
Login("test@example.com", "");
} catch (std::exception& e) {
std::cout << "Handled by main()";
}
}
Handled by Login()
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();
throw e;
}
}
int main() {
try {
func();
} catch (std::exception& e) {
std::cout << "\nSecond Catch: "
<< e.what();
}
}
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 is because throw e;
throws a copy of the exception, which is not desirable. Creating a copy unnecessarily has a performance impact, but worse, it can change the type, and that’s what is happening in the previous program.
This is yet another example of where we can accidentally slice our objects by copying them to a base type.
In this case, we’re copying a std::invalid_argument
to a simpler std::exception
object. In most compilers, the std::exception
type doesn’t store a custom error message, so it is lost in the process.
Unless we explicitly want to change the type of error that is thrown, we should simply use throw;
in isolation:
#include <iostream>
void func() {
try {
throw std::invalid_argument {
"Useful Error Message"
};
} catch (std::exception& e) {
std::cout << "First Catch: "
<< e.what();
throw;
}
}
int main() {
try {
func();
} catch (std::exception& e) {
std::cout << "\nSecond Catch: "
<< e.what();
}
}
First Catch: Useful Error Message
Second Catch: Useful Error Message
We can also rethrow exceptions using more advanced techniques involving std::rethrow_exception()
, which we cover later:
std::exception_ptr
There are scenarios where we might want to capture an exception, pass it to other functions, and potentially rethrow it later. In this regard, there is nothing special about objects that are thrown as part of exceptions. They’re regular objects, and can be stored and transferred as normal:
#include <iostream>
#include <stdexcept>
void HandleException(std::runtime_error& e) {
std::cout << "Handled std::runtime_error";
}
int main() {
try {
throw std::logic_error{"Error"};
} catch (std::runtime_error& e) {
HandleException(e);
}
}
Handled std::runtime_error
However, this technique naturally requires us to know the specific type of exception we’re working with. That is not always viable, and this is where std::exception_ptr
type comes into play.
The std::exception_ptr
is a standard type that can hold a pointer to any type of object that was thrown. It's essentially used as a vehicle to store and transfer exceptions between different parts of a program. It can be thought of as a safe way to handle exceptions outside of the usual try-catch
blocks.
The std::excepion_ptr
type can be null
- that is, not pointing at any exception. We can check if it is null by coercing it to a boolean, in the usual way.
void HandleException(std::exception_ptr e) {
if (e) {
std::cout << "We have an exception";
} else {
std::cout << "There is no exception";
}
}
std::current_exception()
To capture an exception in a std::exception_ptr
, we use the std::current_exception()
function.
catch
block.std::exception_ptr
that holds it.std::exception_ptr
.#include <stdexcept>
#include <iostream>
void HandleException(std::exception_ptr e) {
if (e) {
std::cout << "We have an exception\n";
} else {
std::cout << "There is no exception\n";
}
}
int main() {
try {
throw std::runtime_error{"Error"};
} catch(...) {
HandleException(std::current_exception());
}
// std::current_exception() returns null here
HandleException(std::current_exception());
}
We have an exception
There is no exception
std::rethrow_exception()
Once an exception is captured into a std::exception_ptr
, it can be rethrown later using std::rethrow_exception()
:
std::exception_ptr
as its argument.std::exception_ptr
is not null before calling std::rethrow_exception()
.Below, our HandleException
function rethrows its std::exception_ptr
parameter and immediately handles it. Because we included a default handler using catch(...)
, this HandleException
function can handle any type of exception:
#include <iostream>
#include <stdexcept>
class SomeCustomError {};
void HandleException(std::exception_ptr e) {
if (e) {
try {
std::rethrow_exception(e);
} catch (std::runtime_error& e) {
std::cout << "Handled std::runtime_error";
} catch (SomeCustomError& e) {
std::cout << "Handled SomeCustomError";
} catch (...) {
std::cout << "Handled everything else";
}
}
}
int main() {
try {
throw std::runtime_error{"Error"};
} catch (...) {
HandleException(std::current_exception());
}
}
Handled std::runtime_error
As usual, not every exception needs to be handled immediately. We can let exceptions escape to be handled elsewhere in the stack.
In the previous example, updating our catch(...)
block to simply rethrow the exception would mean HandleException()
takes care of std::runtime_error
and SomeCustomError
only, with everything else being sent back up the stack:
#include <iostream>
#include <stdexcept>
class SomeCustomError {};
void HandleException(std::exception_ptr e) {
if (e) {
try {
std::rethrow_exception(e);
} catch (std::runtime_error& e) {
std::cout << "Handled std::runtime_error";
} catch (SomeCustomError& e) {
std::cout << "Handled SomeCustomError";
} catch (...) {
std::rethrow_exception(e);
}
}
}
int main() {
try {
throw std::logic_error{"Error"};
} catch (...) {
try {
HandleException(std::current_exception());
} catch (...) {
std::cout << "HandleException() couldn't"
" handle that, so main caught it";
}
}
}
HandleException() couldn't handle that, so main caught it
The combination of std::exception_ptr
, std::current_exception()
, and std::rethrow_exception()
is particularly useful in scenarios like:
This lesson has covered techniques to store and rethrow exceptions using advanced techniques. The key topics we learned included:
throw;
to avoid data loss and object slicing.std::exception_ptr
for flexible handling and transfer of exceptions across different parts of a program.std::current_exception()
to capture exceptions and std::rethrow_exception()
to rethrow them as needed.Learn how we can rethrow errors, and wrap exceptions inside other exceptions using standard library helpers.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.