Skip to content
C++
Language
since C++98
Intermediate

Exception Safety Guarantees

C++ exception safety levels — no-throw, strong, basic, and none — and techniques for writing exception-safe code using RAII, copy-and-swap, noexcept, and std::move_if_noexcept.

Exception Safetysince C++98

A guarantee a function provides about program state when it exits via exception: whether invariants hold, resources are freed, and state is left unchanged.

Overview

Exception safety is a contract between a function and its callers. There are four levels, from strongest to weakest:

LevelGuaranteeTypical mechanism
No-throwNever throws; noexceptTrivial ops, destructors, swaps
StrongSucceeds or leaves state unchangedCopy-and-swap, transactional update
BasicNo leaks, invariants hold, state may changeRAII, smart pointers
NoneAnything goes on throwAvoid

The no-throw guarantee predates C++11's noexcept keyword — it was expressed with throw() in C++98/03. Since C++11, noexcept is the correct spelling; throw() was deprecated in C++11 and removed in C++17. A noexcept function that does throw causes std::terminate — there is no stack unwinding.

Aim for strong where feasible, always guarantee basic. No-throw is required for destructors, swaps, and move operations used by the standard library's containers.


Examples

RAII: the baseline for basic guarantee

Every resource acquisition should be tied to an object's lifetime. This makes leaks structurally impossible regardless of exceptions.

cpp
// C++98: manual resource management — no guarantee
void bad_init() {
    int* a = new int[100];
    int* b = new int[200];  // throws: a leaks
    use(a, b);
    delete[] b;
    delete[] a;
}

// C++11: unique_ptr gives basic/strong for free
void safe_init() {
    auto a = std::make_unique<int[]>(100);  // C++14
    auto b = std::make_unique<int[]>(200);  // if throws, a freed automatically
    use(a.get(), b.get());
}

Copy-and-swap: canonical strong guarantee for assignment

cpp
class Buffer {
    std::unique_ptr<int[]> data_;  // C++11
    std::size_t n_;

public:
    Buffer(std::size_t n)
        : data_{std::make_unique<int[]>(n)}, n_{n} {}  // C++14

    // Copy constructor: may throw — that's fine, *this not yet constructed
    Buffer(const Buffer& rhs)
        : data_{std::make_unique<int[]>(rhs.n_)}, n_{rhs.n_} {
        std::copy_n(rhs.data_.get(), n_, data_.get());
    }

    // Swap: noexcept — swapping primitives and unique_ptr internals never throws
    friend void swap(Buffer& a, Buffer& b) noexcept {  // C++11 noexcept
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.n_, b.n_);
    }

    // Strong guarantee: if copy throws, *this is unchanged
    Buffer& operator=(Buffer rhs) {  // rhs constructed by caller (may throw)
        swap(*this, rhs);            // noexcept — cannot fail
        return *this;                // old data destroyed in rhs's dtor
    }
};

The pass-by-value assignment operator handles both copy and move assignment. If rhs construction throws, *this was never touched.

Move operations must be noexcept for containers to use them

std::vector copies elements during reallocation unless the move constructor is noexcept. If it is not, the vector falls back to copying to preserve the strong guarantee.

cpp
class Widget {
    std::vector<int> data_;

public:
    // C++11: noexcept move — vector reallocation will use this
    Widget(Widget&&) noexcept = default;
    Widget& operator=(Widget&&) noexcept = default;
};

// std::move_if_noexcept (C++11): move if safe, copy otherwise
template<typename T>
void reallocate(T* old_buf, T* new_buf, std::size_t n) {
    for (std::size_t i = 0; i < n; ++i)
        ::new(new_buf + i) T(std::move_if_noexcept(old_buf[i]));
}

Transactional update: build then commit

When modifying a complex object, construct the new state entirely before touching the original.

cpp
struct Config {
    std::string path;
    std::vector<std::string> plugins;
    void validate() const;  // may throw if invalid
};

// Strong guarantee: cfg unchanged if anything throws
void apply_config(Config& cfg, const Config& patch) {
    Config tmp = cfg;          // copy — may throw; cfg untouched
    tmp.path = patch.path;     // may throw
    tmp.plugins = patch.plugins;  // may throw
    tmp.validate();            // may throw

    // Commit: only noexcept operations from here
    using std::swap;
    swap(cfg, tmp);            // noexcept if Config::swap is noexcept
}

std::exchange for move assignment (C++14)

cpp
class FileHandle {
    FILE* f_ = nullptr;

public:
    FileHandle& operator=(FileHandle&& rhs) noexcept {  // C++11
        // Close our file, take ownership of rhs, leave rhs null
        if (f_) std::fclose(f_);
        f_ = std::exchange(rhs.f_, nullptr);  // C++14
        return *this;
    }

    ~FileHandle() { if (f_) std::fclose(f_); }
};

The pop() problem and its solution

cpp
class SafeStack {
    std::vector<int> data_;

public:
    // PROBLEM: cannot provide strong guarantee for combined top+pop
    // because the copy in `return data_.back()` could throw after pop_back
    int unsafe_pop() {
        int val = data_.back();  // copy — may throw
        data_.pop_back();        // noexcept — but copy already failed
        return val;
    }

    // SOLUTION 1: separate operations (std::stack design)
    const int& top() const noexcept { return data_.back(); }
    void pop() noexcept { data_.pop_back(); }

    // SOLUTION 2: move out — noexcept for int, safe for move-only types
    int pop_move() {
        int val = std::move(data_.back());  // C++11; noexcept for int
        data_.pop_back();                   // noexcept
        return val;
    }
};

Best Practices

Mark moves, swaps, and destructors noexcept.
The standard library only uses move operations during container reallocation if they are noexcept. Omitting it silently degrades performance and can break strong guarantees elsewhere. Destructors that throw cause std::terminate during stack unwinding — never let them throw.

Prefer make_unique / make_shared over raw new.
new T and new U in a single expression have an unspecified evaluation order; if new U throws after new T succeeds but before the shared_ptr takes ownership, T leaks. make_shared<T>(...) eliminates this window. (C++14 introduced make_unique; make_shared has been available since C++11.)

Declare member initializer order to match destruction order.
Members are constructed in declaration order and destroyed in reverse. If a later member's constructor throws, all previously constructed members are destroyed automatically — RAII does the cleanup without any try/catch.

Do work, then commit with noexcept.
Split every mutating operation into a throwing preparation phase and a noexcept commit phase. The transactional update pattern above is the canonical form.

Use noexcept(noexcept(...)) for generic code.
When writing templates, propagate the noexcept status of wrapped operations:

cpp
template<typename T>
void swap_values(T& a, T& b)
    noexcept(noexcept(std::swap(a, b)))  // C++11
{
    std::swap(a, b);
}

Common Pitfalls

Throwing in a destructor during stack unwinding calls std::terminate.
If a destructor throws while another exception is propagating, the program terminates. Destructors must be noexcept (they are implicitly noexcept since C++11 unless explicitly specified otherwise or a member's destructor throws).

Forgetting noexcept on move constructors defeats vector optimisation.
A vector growing its buffer will copy every element — O(n) copies instead of O(n) moves — if the element's move constructor is not noexcept. Use = default where possible; the compiler will infer noexcept correctly if all members are noexcept-movable.

Self-assignment in the copy-assign operator.
The copy-and-swap idiom handles self-assignment correctly by design — the copy is made before touching *this. Explicit if (this == &rhs) checks are a code smell that often masks an otherwise broken operator.

Assuming std::list operations never throw.
std::list splice and sort operations do offer strong or noexcept guarantees, but assignment operators offer only basic. Do not assume every list operation is nothrow.

Conflating noexcept with "cannot fail".
noexcept is a promise to the caller; violating it calls std::terminate. It does not prevent failure — it changes how failure is handled. Mark a function noexcept only when you are certain no exception will propagate out of it under any reachable condition.


The Four Exception Safety Guarantees

Every function falls into exactly one of four levels. The levels are ordered by strength; stronger guarantees cost more to implement but protect callers more completely.

LevelWhat the caller can count onTypical cost
No-throwFunction never propagates an exception; noexcept on the declarationRestrict to operations that genuinely cannot fail
StrongSucceeds completely or leaves state byte-for-byte unchanged (commit-or-rollback)Extra copy or transactional staging area
BasicNo resource leaks, all invariants hold, but observable state may differRAII ownership — usually achievable for free
NoneAnything can happen — leaks, partial writes, broken invariantsLegacy C-style code; avoid in new code
cpp
// C++11 — no-throw guarantee: noexcept prevents unwinding
int safe_divide(int a, int b) noexcept {
    return (b != 0) ? a / b : 0;  // caller chose to suppress the error
}

// C++11 — strong guarantee: copy-and-commit pattern
void append_unique(std::vector<int>& v, int x) {
    if (std::find(v.begin(), v.end(), x) == v.end()) {
        v.push_back(x);  // may throw; if it does, v is unchanged (push_back is strong)
    }
}

// C++11 — basic guarantee: RAII prevents leaks even if parsing throws
std::vector<int> parse_ints(std::istream& in) {
    std::vector<int> result;
    int n;
    while (in >> n)
        result.push_back(n);   // may throw; result will be destroyed cleanly
    return result;
}

// No guarantee — avoid
void dangerous(int** pp, std::size_t n) {
    *pp = new int[n];           // if next line throws after this, *pp leaks
    throw std::runtime_error("oops");
}

No-throw is required, not merely preferred, in three places: destructors (the runtime calls them during unwinding), swap functions (called by standard containers), and move constructors used inside containers. Anywhere else, aim for strong; always guarantee basic.


Copy-and-Swap: The Strong Guarantee Pattern

The copy-and-swap idiom converts a potentially throwing copy into a noexcept swap, giving assignment the strong guarantee automatically. The trick is that the copy happens outside *this — if it throws, *this has not been touched.

cpp
// C++11
class Buffer {
    std::unique_ptr<int[]> data_;
    std::size_t            n_;

public:
    explicit Buffer(std::size_t n)
        : data_{std::make_unique<int[]>(n)}, n_{n} {}

    // Step 1 — copy constructor may throw; that is acceptable because
    // the object under construction has no previous state to corrupt.
    Buffer(const Buffer& rhs)
        : data_{std::make_unique<int[]>(rhs.n_)}, n_{rhs.n_} {
        std::copy_n(rhs.data_.get(), n_, data_.get());
    }

    // Step 2 — swap is noexcept: only primitive and unique_ptr moves occur.
    friend void swap(Buffer& a, Buffer& b) noexcept {  // C++11
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.n_,    b.n_);
    }

    // Step 3 — assignment takes rhs BY VALUE (copy or move done by compiler).
    // If the copy throws the exception propagates before *this is modified.
    // The swap is noexcept, so the commit phase cannot fail.
    Buffer& operator=(Buffer rhs) noexcept {  // C++11
        swap(*this, rhs);   // noexcept: cannot throw
        return *this;       // old data released in rhs destructor
    }

    // Move constructor: noexcept so vector reallocation moves, not copies.
    Buffer(Buffer&&) noexcept = default;  // C++11
};

T& operator=(T rhs) (pass-by-value) handles both copy-assignment and move-assignment with a single overload. When the caller passes an lvalue the compiler copies into rhs; when it passes an rvalue the compiler moves. Either way, by the time the body executes the expensive work is already done and *this has not been touched.


noexcept in Move Operations and std::vector

std::vector::push_back and reserve move elements during reallocation only when the element's move constructor is noexcept; otherwise they copy. This is the "noexcept-or-copy" rule: copying preserves the strong guarantee because the original elements remain intact if the copy of a later element throws; moving does not, so moving is only used when the move cannot throw.

cpp
// C++11 — BAD: move constructor is not noexcept; vector will copy on realloc
class Expensive {
    std::vector<double> data_;
public:
    Expensive(Expensive&&) {}              // not noexcept — copy fallback used!
    Expensive& operator=(Expensive&&) {}   // same
};

// C++11 — GOOD: noexcept move; vector uses move during realloc
class Cheap {
    std::vector<double> data_;
public:
    Cheap(Cheap&&) noexcept = default;             // C++11
    Cheap& operator=(Cheap&&) noexcept = default;  // C++11
};

// Verify at compile time that move is truly noexcept.
static_assert(std::is_nothrow_move_constructible_v<Cheap>,   // C++17
              "Cheap must be noexcept-move-constructible for vector to move it");
static_assert(std::is_nothrow_move_assignable_v<Cheap>,      // C++17
              "Cheap must be noexcept-move-assignable");

The performance impact is significant: a std::vector<Expensive> that grows from capacity 1 to 1 000 000 elements copies every existing element at each reallocation step, making total work O(n²) in the number of reallocations. With noexcept move constructors, reallocation is O(n) total.

cpp
// C++11 — std::move_if_noexcept used internally by vector
template<typename T>
void relocate(T* src, T* dst, std::size_t n) {
    for (std::size_t i = 0; i < n; ++i)
        ::new(dst + i) T(std::move_if_noexcept(src[i]));
        // Moves if T's move ctor is noexcept; copies otherwise.
}

Exception Safety in Generic Code (Templates)

Generic code should be exception-neutral: it propagates exceptions from user-supplied types without corrupting its own state. This means catching exceptions only to undo partial work and rethrowing — never swallowing them.

cpp
// C++11 — exception-neutral algorithm: rollback on failure, then rethrow
template<typename ForwardIt, typename T>
void fill_n_strong(ForwardIt first, std::size_t n, const T& val) {
    std::size_t i = 0;
    try {
        for (; i < n; ++i, ++first)
            *first = val;   // assignment may throw
    } catch (...) {
        // Undo writes completed so far to restore the range.
        ForwardIt rollback = first;
        std::advance(rollback, -static_cast<std::ptrdiff_t>(i));
        for (std::size_t j = 0; j < i; ++j, ++rollback)
            *rollback = T{};   // best-effort reset; noexcept assumed for T{}
        throw;  // rethrow the original exception — never swallow in generic code
    }
}

Exception neutrality requires that cleanup operations in catch (...) blocks themselves not throw — use noexcept cleanup where possible.

cpp
// C++11 — conditional noexcept: std::swap is noexcept when element swap is
template<typename T>
void swap_ranges_safe(T* a, T* b, std::size_t n)
    noexcept(noexcept(std::swap(*a, *b)))  // propagate T's swap noexcept status
{
    for (std::size_t i = 0; i < n; ++i)
        std::swap(a[i], b[i]);
}

// std::swap itself uses conditional noexcept in its declaration:
//   template<typename T>
//   void swap(T& a, T& b)
//       noexcept(std::is_nothrow_move_constructible_v<T> &&
//                std::is_nothrow_move_assignable_v<T>);

Exception neutrality is the generic code counterpart of the strong guarantee: your template cannot know whether user types will throw, so the only safe contract is "I will not make things worse if they do."


RAII Wrappers as the Fundamental Exception Safety Tool

RAII (Resource Acquisition Is Initialization) means that every resource is owned by an object whose destructor releases it. Because destructors run deterministically during stack unwinding, RAII makes resource leaks structurally impossible regardless of which path an exception takes.

cpp
// C++11 — std::unique_ptr: RAII for heap memory
void process(std::size_t n) {
    auto buf = std::make_unique<int[]>(n);  // C++14; buf owns the array
    heavy_computation(buf.get(), n);        // may throw
    // buf destroyed here whether we return normally or by exception
}

// C++11 — RAII file handle: no try/catch needed
class FileGuard {
    std::FILE* f_;
public:
    explicit FileGuard(const char* path, const char* mode)
        : f_{std::fopen(path, mode)} {
        if (!f_) throw std::system_error(errno, std::generic_category());
    }
    ~FileGuard() { if (f_) std::fclose(f_); }  // noexcept by definition
    std::FILE* get() const noexcept { return f_; }
    FileGuard(const FileGuard&) = delete;
    FileGuard& operator=(const FileGuard&) = delete;
};

For ad-hoc cleanup that does not justify a full class, use a scope guard — an object whose destructor calls a user-supplied callable unconditionally.

cpp
// C++23 — std::scope_exit from <scope> (unconditional cleanup on scope exit)
#include <scope>   // C++23
void register_and_use(Registry& reg, Handle h) {
    reg.add(h);
    std::scope_exit cleanup{[&] { reg.remove(h); }};  // C++23
    // ... work that may throw ...
    // cleanup runs whether we return normally or by exception
    cleanup.release();  // cancel cleanup on success path if needed
}

// C++11 — minimal ScopeGuard for pre-C++23 codebases
template<typename F>
class ScopeGuard {
    F func_;
    bool active_ = true;
public:
    explicit ScopeGuard(F f) : func_{std::move(f)} {}
    ~ScopeGuard() { if (active_) func_(); }
    void release() noexcept { active_ = false; }
    ScopeGuard(const ScopeGuard&) = delete;
};
template<typename F> ScopeGuard<F> make_scope_guard(F f) {
    return ScopeGuard<F>{std::move(f)};
}

RAII eliminates the majority of try/catch blocks that exist only for cleanup. Reserve catch for actual error handling — translating an exception type, logging, or recovering with an alternative strategy — not for freeing resources.


Exception Safety vs Performance: noexcept Tradeoffs

noexcept is a performance hint as well as a safety contract. Compilers can omit exception-handling bookkeeping (unwinding tables, landing-pad generation) for noexcept functions, and the standard library uses noexcept status to choose faster code paths at compile time.

cpp
// C++11 — noexcept enables compiler to omit unwind metadata
int fast_sum(const int* p, std::size_t n) noexcept {
    int total = 0;
    for (std::size_t i = 0; i < n; ++i) total += p[i];
    return total;
}

// C++11 — conditional noexcept: propagate the wrapped operation's status
template<typename T>
T safe_deref(const T* p, T fallback)
    noexcept(std::is_nothrow_copy_constructible_v<T>)  // C++11/17
{
    return p ? *p : fallback;
}

// C++11 — noexcept(noexcept(expr)): query an expression's noexcept status
template<typename Container>
void clear_and_swap(Container& a, Container& b)
    noexcept(noexcept(a.swap(b)))   // true iff Container::swap is noexcept
{
    a.clear();
    b.clear();
    a.swap(b);
}

Where noexcept should always appear:

Function kindRuleReason
DestructorAlways noexcept (implicit since C++11)Throwing during unwinding calls std::terminate
Move constructornoexcept when possiblestd::vector reallocation moves only if noexcept
Move assignmentnoexcept when possibleSame; also enables std::swap optimisations
swapAlways noexceptStandard algorithms and containers assume nothrow swap
Observer / getternoexcept if body cannot throwDocuments intent; enables inlining optimisations
noexcept(noexcept(expr))Generic wrappersPropagates inner type's status; avoids false promises

Marking a function noexcept when it can actually throw does not suppress the exception — it calls std::terminate instead. Only mark noexcept when you have verified (or static_assert-ed) that the body and everything it calls is genuinely noexcept.

cpp
// C++17 — verify before promising
class Vec3 {
    float x_, y_, z_;
public:
    Vec3(Vec3&&) noexcept = default;  // safe: float move is trivially noexcept

    // Check that a type-erased wrapper is truly noexcept before marking it so.
    template<typename T>
    static T load(const float* p) noexcept(
        std::is_nothrow_constructible_v<T, const float*>)  // C++17
    {
        return T{p};
    }
};

static_assert(std::is_nothrow_move_constructible_v<Vec3>);  // C++17 — fails fast if wrong

See Also