Storing and Rethrowing Exceptions

This lesson offers a comprehensive guide to storing and rethrowing exceptions
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Edited

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.

Rethrowing Exceptions

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()

Conditionally Rethrowing

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()

Preventing Slicing (Data Loss) when Rethrowing

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:

Understanding 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";
  }
}

Capturing Exceptions with std::current_exception()

To capture an exception in a std::exception_ptr, we use the std::current_exception() function.

  • This function is called within a catch block.
  • It captures the currently handled exception and returns a std::exception_ptr that holds it.
  • If no exception is being handled, it returns a null 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

Rethrowing with std::rethrow_exception()

Once an exception is captured into a std::exception_ptr, it can be rethrown later using std::rethrow_exception():

  • This function takes a std::exception_ptr as its argument.
  • When called, it rethrows the exception that was previously captured.
  • We should ensure that the 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

Practical Use Cases

The combination of std::exception_ptr, std::current_exception(), and std::rethrow_exception() is particularly useful in scenarios like:

  • Deferred exception handling, where an exception needs to be transported to a different context for handling.
  • Complex systems where exceptions need to be passed across different layers or threads.
  • When an exception must be stored temporarily for logging or auditing purposes before being rehandled or propagated.

Summary

This lesson has covered techniques to store and rethrow exceptions using advanced techniques. The key topics we learned included:

  • Rethrowing exceptions correctly using throw; to avoid data loss and object slicing.
  • Practical uses, including conditional rethrowing to handle specific error conditions while passing others up the call stack.
  • Utilizing std::exception_ptr for flexible handling and transfer of exceptions across different parts of a program.
  • Understanding and using std::current_exception() to capture exceptions and std::rethrow_exception() to rethrow them as needed.

Was this lesson useful?

Edit History

  • Refreshed content and moved nested exceptions to a dedicated lesson

  • First Published

Ryan McCombe
Ryan McCombe
Edited
7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Exceptions and Error Handling
7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 114 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Nested Exceptions

Learn about nested exceptions in C++: from basic concepts to advanced handling techniques
3D Character Concept Art
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved