Skip to content

Exception Safety — noexcept, RAII Guarantees, Strong and Basic Guarantees, Exception-Safe Code

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Exception Safety. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ exception safety guarantees define how functions behave under exceptions — the no-throw guarantee, strong guarantee (atomic commit or rollback), basic guarantee (no leaks, valid state), and no guarantee — each with increasing risk.

What You'll Learn

You will understand the four exception safety levels and apply them to your code, use RAII to provide automatic cleanup during unwinding, write strong-guarantee functions using the copy-and-swap idiom, apply noexcept for move operations and swap, detect and fix exception-unsafe code patterns, and design interfaces that document their exception safety.

Why It Matters

Exception safety separates professional C++ from hobbyist code. Without it, a single exception can leak memory, corrupt data, or leave objects in invalid states. C++'s Exception Handling mechanism (stack unwinding) is powerful but unforgiving — RAII is the only reliable way to manage resources during exceptions. The STL itself requires containers to meet specific safety levels.

Learning Path

graph LR
    A["58: Modules"] --> B["59: Exception Safety"]
    B --> C["60: RAII & Resource Management"]
    C --> D["61: Design Patterns in C++"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

The Four Exception Safety Levels

From safest to least safe:

  1. No-throw guarantee: The function will never throw.
  2. Strong guarantee: If an exception occurs, the program state is unchanged (atomic commit).
  3. Basic guarantee: No resources are leaked and objects remain in a valid (but unspecified) state.
  4. No guarantee: Anything can happen (avoid writing code at this level).
#include <iostream>
#include <vector>
#include <stdexcept>

class Account {
    std::string name_;
    double balance_;
public:
    Account(std::string name, double balance)
        : name_(std::move(name)), balance_(balance) {}

    // No-throw guarantee: never throws
    double getBalance() const noexcept {
        return balance_;
    }

    // Strong guarantee: either succeeds or balance is unchanged
    void deposit(double amount) {
        if (amount < 0) throw std::invalid_argument("negative deposit");
        // No operations that could fail after this point (ideally)
        balance_ += amount;
    }

    // Basic guarantee: valid state, but could be partial
    void transferTo(Account& other, double amount) {
        // If withdraw throws, we're fine (no-op)
        // If deposit throws, withdraw has already happened!
        // This has only basic guarantee (state is valid but ambiguous)
        balance_ -= amount;      // Could make balance negative
        other.deposit(amount);   // If this throws, both accounts are wrong
    }

    // Strong-guarantee version via copy-and-swap
    static void transfer(Account& from, Account& to, double amount) {
        Account tempFrom = from;   // Copy
        Account tempTo = to;       // Copy
        tempFrom.balance_ -= amount;
        tempTo.balance_ += amount;
        // Only now commit: if swap throws, nothing changes
        std::swap(from, tempFrom);
        std::swap(to, tempTo);
    }
};

noexcept Specifier

noexcept declares that a function will not throw exceptions. The compiler can optimize more aggressively.

#include <iostream>
#include <type_traits>
#include <utility>

// noexcept declaration
void safeFunction() noexcept {
    // If an exception propagates here, std::terminate is called
}

// Conditional noexcept
template <typename T>
void swap(T& a, T& b) noexcept(std::is_nothrow_swappable_v<T>) {
    using std::swap;
    swap(a, b);
}

// noexcept is part of the function type
void mayThrow();
void noThrow() noexcept;

int main() {
    std::cout << "mayThrow is noexcept: "
              << noexcept(mayThrow()) << "\n";     // false
    std::cout << "noThrow is noexcept: "
              << noexcept(noThrow()) << "\n";       // true

    // noexcept operator: compile-time check
    auto lambda1 = []() noexcept {};
    auto lambda2 = []() {};

    std::cout << "lambda1: " << noexcept(lambda1()) << "\n";  // true
    std::cout << "lambda2: " << noexcept(lambda2()) << "\n";  // false
}

Copy-and-Swap Idiom

The copy-and-swap idiom provides the strong exception safety guarantee for assignment.

#include <iostream>
#include <cstring>
#include <algorithm>

class String {
    char* data_;
    size_t size_;

    void swap(String& other) noexcept {
        std::swap(data_, other.data_);
        std::swap(size_, other.size_);
    }

public:
    String(const char* str) : size_(std::strlen(str)), data_(new char[size_ + 1]) {
        std::strcpy(data_, str);
    }

    // Copy constructor
    String(const String& other) : size_(other.size_), data_(new char[size_ + 1]) {
        std::strcpy(data_, other.data_);
    }

    // Destructor
    ~String() { delete[] data_; }

    // Copy-and-swap assignment (strong guarantee)
    String& operator=(String other) noexcept {  // 'other' is passed by value (copy)
        swap(other);                             // No-throw swap
        return *this;                            // Old data destroyed with 'other'
    }

    // Move constructor
    String(String&& other) noexcept
        : data_(std::exchange(other.data_, nullptr)), size_(other.size_) {}

    const char* c_str() const { return data_; }
};

int main() {
    String s1("hello");
    String s2("world");

    s1 = s2;  // Copy-and-swap: strong guarantee
    std::cout << s1.c_str() << "\n";  // world

    s1 = String("temporary");  // Move assignment (via the same operator)
    std::cout << s1.c_str() << "\n";  // temporary
}

RAII and Exception Safety

RAII is the foundation of exception safety. Destructors run during stack unwinding.

#include <iostream>
#include <fstream>
#include <stdexcept>

// RAII wrapper for FILE*
class File {
    FILE* file_;
public:
    File(const char* filename, const char* mode)
        : file_(std::fopen(filename, mode)) {
        if (!file_) throw std::runtime_error("Cannot open file");
    }

    ~File() {
        if (file_) std::fclose(file_);
    }

    // Move-only (no copy)
    File(File&& other) noexcept : file_(std::exchange(other.file_, nullptr)) {}
    File& operator=(File&& other) noexcept {
        if (this != &other) {
            if (file_) std::fclose(file_);
            file_ = std::exchange(other.file_, nullptr);
        }
        return *this;
    }

    void write(const std::string& text) {
        if (std::fputs(text.c_str(), file_) == EOF) {
            throw std::runtime_error("Write failed");
        }
    }
};

void processFile() {
    File f("test.txt", "w");
    f.write("Hello, World!\n");
    // If write throws, the File destructor runs and closes the file
    // No resource leak!
}

int main() {
    try {
        processFile();
        std::cout << "File written successfully\n";
    } catch (const std::exception& e) {
        std::cout << "Error: " << e.what() << "\n";
    }
}

std::terminate and Unexpected Exceptions

When a noexcept function throws, std::terminate is called.

#include <iostream>
#include <exception>

void dangerous() noexcept {
    throw std::runtime_error("This should not happen");
    // noexcept is violated: std::terminate will be called
}

int main() {
    std::set_terminate([]() {
        std::cout << "Terminate called!\n";
        std::abort();
    });

    // dangerous();  // Would call terminate

    // noexcept in destructors
    struct BadDestructor {
        ~BadDestructor() noexcept(false) {
            throw std::runtime_error("destructor error");
        }
    };

    // During stack unwinding, a second exception in destructor calls terminate
    try {
        BadDestructor b;
        throw std::runtime_error("main exception");
    } catch (...) {
        // If b's destructor also throws: terminate
        std::cout << "Caught exception\n";
    }
}

Exception Safety in the STL

STL containers provide specific safety guarantees.

#include <iostream>
#include <vector>
#include <list>

int main() {
    // std::vector: strong guarantee for push_back (unless move throws)
    std::vector<int> v = {1, 2, 3};
    try {
        v.push_back(4);  // If reallocation fails, vector is unchanged
    } catch (...) {
        std::cout << "push_back failed, size: " << v.size() << "\n";  // 3
    }

    // std::list: strong guarantee for all operations (no reallocation)
    std::list<int> lst = {1, 2, 3};
    try {
        lst.push_back(4);  // Always succeeds (no reallocation needed)
    } catch (...) {
        // Only fails if memory allocation fails
    }

    // emplace_back: strong guarantee
    struct ThrowsOnCopy {
        ThrowsOnCopy() = default;
        ThrowsOnCopy(const ThrowsOnCopy&) { throw std::runtime_error("copy"); }
    };

    std::vector<ThrowsOnCopy> tricky;
    tricky.emplace_back();  // Construct in place — no copy needed
    // tricky.push_back(ThrowsOnCopy{});  // Would throw during copy
}

Common Mistakes

Mistake 1: Throwing from a destructor

struct Bad {
    ~Bad() {
        throw std::runtime_error("error");  // Never do this!
    }
};

If a destructor throws during stack unwinding (another exception active), std::terminate is called. Destructors should always be noexcept.

Mistake 2: Assuming strong guarantee without copy-and-swap

void update(Data& d) {
    d.field1 = compute1();  // If compute2 throws, field1 is already modified
    d.field2 = compute2();  // State is partially updated!
}

Use copy-and-swap for atomic updates.

Mistake 3: Not marking move operations noexcept

struct Movable {
    Movable(Movable&&) { /* may throw */ }  // No noexcept
};
// std::vector will copy instead of move during reallocation

STL containers prefer copy over move if move is not noexcept (for strong guarantee).

Mistake 4: Resource leak before RAII

void bad() {
    int* p = new int(5);
    func();  // If func throws, p leaks
    delete p;
}

Use std::unique_ptr or other RAII wrappers.

Mistake 5: Swallowing all exceptions

try {
    risky();
} catch (...) {
    // Silently ignores all errors
}

Practice Questions

  1. What are the four exception safety levels? Answer: No-throw, strong (atomic), basic (no leak, valid state), and no guarantee.

  2. Why must destructors be noexcept? Answer: If a destructor throws during stack unwinding (when another exception is active), std::terminate is called.

  3. What is the copy-and-swap idiom? Answer: The assignment operator takes the parameter by value (copy), then swaps with the copy. If the copy throws, *this is unchanged (strong guarantee).

  4. What does noexcept do to function behavior? Answer: It declares the function will not throw. If an exception attempts to propagate, std::terminate is called.

  5. Why does std::vector require noexcept move constructors? Answer: For the strong exception safety guarantee during reallocation. If move throws, vector falls back to copy.

FAQ

What is exception safety in C++

Exception safety defines how code behaves under exceptions. The four levels range from no-throw (safest) to no guarantee (unsafe). RAII is the primary tool for exception-safe code.

What is the strong exception safety guarantee

The strong guarantee ensures atomicity: if an exception occurs, the program state is exactly as before the function was called. Copy-and-swap is the standard implementation pattern.

Why should destructors be noexcept

If a destructor throws during stack unwinding (when another exception is active), std::terminate is called immediately, destroying the program.

Does noexcept affect performance

Yes. The compiler can generate better code when it knows a function won't throw (fewer cleanup blocks, no exception tables). It also enables move semantics in STL containers.

What is the basic exception safety guarantee

The basic guarantee ensures no resources leak and all objects remain in valid (but potentially unspecified) states. No invariant is violated, but the exact state is unpredictable.

Mini Project

Implement a ScopedTransaction class that provides the strong exception safety guarantee for a series of database-style operations:

#include <iostream>
#include <vector>
#include <string>
#include <functional>

// Your ScopedTransaction class

int main() {
    std::vector<std::string> database;

    ScopedTransaction transaction(database);

    transaction.addOperation([&]() { database.push_back("Alice"); });
    transaction.addOperation([&]() { database.push_back("Bob"); });
    transaction.addOperation([&]() {
        database.push_back("Charlie");
        throw std::runtime_error("Simulated failure");
    });

    try {
        transaction.commit();
    } catch (...) {
        std::cout << "Transaction failed, rolled back\n";
    }

    std::cout << "Database size after rollback: " << database.size() << "\n";
    // Should be 0 if rollback worked correctly
}

This project demonstrates how C++ exception safety guarantees apply to real-world transactional systems, similar to database Transaction in Java JDBC or Python's context managers.

What's Next

You now understand exception safety — the discipline that separates robust C++ from fragile code. Next, you will deepen your knowledge of RAII and resource management, the pattern that makes exception safety possible.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro