Skip to main content

Updated Jun 4, 2026

Exception Handling

Exceptions provide a mechanism to transfer control from a point where an error occurs to a handler that can deal with it. They separate error-handling code from normal logic, making code cleaner and more maintainable.

Basic Syntax​

Throwing Exceptions​

#include <stdexcept>
#include <string>

double divide(double a, double b) {
if (b == 0.0) {
throw std::invalid_argument("Division by zero");
}
return a / b;
}

void processFile(const std::string& filename) {
if (filename.empty()) {
throw std::runtime_error("Empty filename");
}
// Process file...
}

Catching Exceptions​

#include <iostream>
#include <exception>

void exampleUsage() {
try {
double result = divide(10.0, 0.0);
std::cout << "Result: " << result << '\n';
}
catch (const std::invalid_argument& e) {
std::cerr << "Invalid argument: " << e.what() << '\n';
}
catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << '\n';
}
catch (...) {
std::cerr << "Unknown error occurred\n";
}
}
info

Catch handlers are checked in order. Always catch more specific exceptions before general ones.

Exception Flow​

Standard Exception Hierarchy​

Common Standard Exceptions​

ExceptionUse CaseExample
std::invalid_argumentInvalid function argumentNegative size
std::out_of_rangeIndex out of boundsvec.at(100)
std::runtime_errorRuntime conditionFile not found
std::logic_errorProgramming logic errorPrecondition violated
std::bad_allocMemory allocation failednew failed
std::bad_castDynamic cast failedInvalid dynamic_cast
#include <stdexcept>
#include <vector>

void standardExceptions() {
std::vector<int> vec = {1, 2, 3};

try {
// Throws std::out_of_range
int value = vec.at(10);
}
catch (const std::out_of_range& e) {
std::cerr << "Out of range: " << e.what() << '\n';
}

try {
// Throws std::bad_alloc if allocation fails
int* huge = new int[1000000000000];
}
catch (const std::bad_alloc& e) {
std::cerr << "Allocation failed: " << e.what() << '\n';
}
}

Custom Exceptions​

Simple Custom Exception​

#include <exception>
#include <string>

class FileError : public std::runtime_error {
std::string filename_;

public:
FileError(const std::string& msg, const std::string& filename)
: std::runtime_error(msg), filename_(filename) {}

const std::string& filename() const { return filename_; }
};

void useCustomException() {
try {
throw FileError("Cannot open file", "config.txt");
}
catch (const FileError& e) {
std::cerr << "File error: " << e.what()
<< " (file: " << e.filename() << ")\n";
}
}

Exception with Additional Context​

#include <exception>
#include <string>

class DatabaseError : public std::runtime_error {
int error_code_;
std::string query_;

public:
DatabaseError(const std::string& msg, int code, const std::string& query)
: std::runtime_error(msg), error_code_(code), query_(query) {}

int errorCode() const { return error_code_; }
const std::string& query() const { return query_; }
};

void databaseOperation() {
try {
// Database operation fails
throw DatabaseError("Query failed", 1064, "SELECT * FROM users");
}
catch (const DatabaseError& e) {
std::cerr << "DB Error " << e.errorCode() << ": " << e.what() << '\n'
<< "Query: " << e.query() << '\n';
}
}
Custom Exception Hierarchy

Derive custom exceptions from standard exception classes to maintain compatibility with generic handlers.

Stack Unwinding​

When an exception is thrown, the stack is unwound - local objects are destroyed in reverse order of construction:

#include <iostream>

class Resource {
std::string name_;

public:
Resource(const std::string& name) : name_(name) {
std::cout << "Acquiring " << name_ << '\n';
}

~Resource() {
std::cout << "Releasing " << name_ << '\n';
}
};

void demonstrateUnwinding() {
try {
Resource r1("Resource 1");
Resource r2("Resource 2");

throw std::runtime_error("Error!");

Resource r3("Resource 3"); // Never constructed
}
catch (const std::exception& e) {
std::cout << "Caught: " << e.what() << '\n';
}
// Output:
// Acquiring Resource 1
// Acquiring Resource 2
// Releasing Resource 2
// Releasing Resource 1
// Caught: Error!
}
warning

Stack unwinding calls destructors but does not execute code after the throw point in the current scope.

RAII and Exception Safety​

RAII (Resource Acquisition Is Initialization) ensures resources are properly released during stack unwinding:

#include <fstream>
#include <memory>
#include <mutex>

void exceptionSafeFunction() {
// File automatically closed on exception
std::ifstream file("data.txt");

// Memory automatically freed on exception
auto ptr = std::make_unique<int[]>(100);

// Mutex automatically unlocked on exception
std::mutex mtx;
std::lock_guard<std::mutex> lock(mtx);

// If exception thrown here, all resources cleaned up
if (someCondition()) {
throw std::runtime_error("Error");
}

// Resources released automatically at scope exit
}

Manual Resource Management (Unsafe)​

void unsafeFunction() {
int* data = new int[100];
FILE* file = fopen("data.txt", "r");

// If exception thrown here, memory leaked!
processData(data, file);

delete[] data; // May not execute
fclose(file); // May not execute
}

RAII Resource Management (Safe)​

#include <memory>
#include <fstream>

void safeFunction() {
auto data = std::make_unique<int[]>(100);
std::ifstream file("data.txt");

// Exception safe - resources automatically cleaned up
processData(data.get(), file);

// Automatic cleanup
}
RAII Wrappers

Always use RAII wrappers (smart pointers, file streams, lock guards) for exception-safe resource management.

Exception Specifications​

noexcept Specifier​

Mark functions that don't throw exceptions:

// Guarantees no exceptions
void safeFunction() noexcept {
// If exception thrown, std::terminate called
}

// Conditionally noexcept
template<typename T>
void swap(T& a, T& b) noexcept(std::is_nothrow_move_constructible_v<T>) {
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}

See noexcept and Strong Guarantee for details.

Rethrowing Exceptions​

#include <exception>

void processWithLogging() {
try {
riskyOperation();
}
catch (const std::exception& e) {
logError(e.what());
throw; // Rethrow the same exception
}
}

void convertException() {
try {
lowLevelOperation();
}
catch (const LowLevelError& e) {
// Throw different exception
throw HighLevelError("High-level operation failed", e.what());
}
}
info

Use throw; without an argument to rethrow the original exception object, preserving its type and state.

Nested Exceptions (C++11)​

#include <exception>
#include <stdexcept>

void nestedExceptionExample() {
try {
try {
throw std::runtime_error("Low-level error");
}
catch (...) {
std::throw_with_nested(std::runtime_error("High-level error"));
}
}
catch (const std::exception& e) {
std::cerr << e.what() << '\n';

try {
std::rethrow_if_nested(e);
}
catch (const std::exception& nested) {
std::cerr << " Caused by: " << nested.what() << '\n';
}
}
}

Exception safety guarantees​

How much a function promises about program state when an exception escapes — basic, strong, or no-throw — is a subject of its own. The four levels, the copy-and-swap commit pattern, and the rollback technique are covered on the Exception Safety page. The one-line version: do the throwing work on a copy, then commit with a noexcept move or swap.

void strong(std::vector<int>& vec, int value) {
std::vector<int> temp = vec; // work on a copy (may throw)
temp.push_back(value);
vec = std::move(temp); // commit — noexcept, so this can't fail
}

Performance Considerations​

#include <chrono>

// Exception path (zero-cost until thrown)
double divideException(double a, double b) {
if (b == 0.0) {
throw std::invalid_argument("Division by zero");
}
return a / b;
}

// Return code path (always check)
bool divideReturnCode(double a, double b, double& result) {
if (b == 0.0) {
return false;
}
result = a / b;
return true;
}
info

Zero-cost exceptions: No overhead on the happy path (when no exception is thrown). Overhead only when exception is actually thrown.

Destructors and Exceptions​

danger

NEVER throw exceptions from destructors! If a destructor throws during stack unwinding, std::terminate is called.

class BadExample {
public:
~BadExample() {
// WRONG: May throw during unwinding
throw std::runtime_error("Error in destructor");
}
};

class GoodExample {
public:
~GoodExample() noexcept {
try {
cleanup(); // May throw
}
catch (...) {
// Log error, but don't rethrow
logError("Cleanup failed");
}
}
};

Exception-Neutral Code​

Code that doesn't handle exceptions but allows them to propagate:

template<typename T>
void processContainer(std::vector<T>& vec) {
for (auto& item : vec) {
item.process(); // May throw - let it propagate
}
// Exception-neutral: doesn't catch, but maintains invariants
}

Practical Examples​

Example 1: File Processing​

#include <fstream>
#include <string>
#include <stdexcept>

class FileProcessor {
public:
void process(const std::string& filename) {
std::ifstream file(filename);
if (!file) {
throw std::runtime_error("Cannot open file: " + filename);
}

std::string line;
int lineNum = 0;

while (std::getline(file, line)) {
++lineNum;
try {
processLine(line);
}
catch (const std::exception& e) {
throw std::runtime_error(
"Error at line " + std::to_string(lineNum) +
": " + e.what()
);
}
}
}

private:
void processLine(const std::string& line);
};

Example 2: Transaction Pattern​

#include <vector>

class Transaction {
std::vector<std::function<void()>> rollback_actions_;
bool committed_ = false;

public:
template<typename Action, typename Rollback>
void execute(Action action, Rollback rollback) {
action();
rollback_actions_.push_back(rollback);
}

void commit() {
committed_ = true;
rollback_actions_.clear();
}

~Transaction() {
if (!committed_) {
// Rollback in reverse order
for (auto it = rollback_actions_.rbegin();
it != rollback_actions_.rend(); ++it) {
try {
(*it)();
}
catch (...) {
// Log but continue rollback
}
}
}
}
};

void useTransaction() {
Transaction txn;

txn.execute(
[]{ createFile("temp.txt"); },
[]{ deleteFile("temp.txt"); }
);

txn.execute(
[]{ allocateResource(); },
[]{ freeResource(); }
);

txn.commit(); // Success - no rollback
}

Example 3: Exception Guard​

template<typename Func>
class ExceptionGuard {
Func func_;
bool dismissed_ = false;

public:
explicit ExceptionGuard(Func f) : func_(std::move(f)) {}

~ExceptionGuard() {
if (!dismissed_) {
try {
func_();
}
catch (...) {
// Suppress exception in destructor
}
}
}

void dismiss() { dismissed_ = true; }
};

void useGuard() {
std::vector<int> vec;

ExceptionGuard guard([&]{ vec.clear(); });

vec.push_back(1);
vec.push_back(2);

if (success) {
guard.dismiss(); // Keep changes
}
// Otherwise, guard will clear vec on destruction
}

When to Use Exceptions​

Use Exceptions When:
  • Errors are exceptional (not expected in normal flow)
  • Error handling code would clutter normal logic
  • Errors need to propagate through multiple layers
  • You need strong exception safety guarantees
  • Constructors fail (can't return error codes)
warning

Avoid Exceptions When:

  • Performance is critical and errors are frequent
  • Writing real-time or embedded systems
  • Interacting with C libraries
  • Destructors or cleanup code
  • noexcept functions

Best Practices​

DO:
  • Throw by value, catch by const reference
  • Use standard exception types when possible
  • Provide meaningful error messages
  • Maintain exception safety guarantees
  • Document which exceptions functions may throw
  • Use RAII for all resource management
danger

DON'T:

  • Throw exceptions from destructors
  • Catch exceptions by value (causes slicing)
  • Use exceptions for control flow
  • Throw pointers to exception objects
  • Ignore exceptions with empty catch blocks
  • Mix exception handling with resource management