Exception Safety

Catching an exception tells you the operation failed; exception safety tells you what state your objects are left in afterward -- and getting that answer wrong is how a single thrown exception corrupts a container that looked fine seconds before.

Throwing and catching an exception is only half the story. The other half is: what shape is the rest of your program in once the exception has been caught? If a function was halfway through updating a data structure when it threw, did it leave that structure in some half-modified, inconsistent state, or did it either finish cleanly or leave everything exactly as it found it? Exception safety is the name for this property, and C++ has settled on a small vocabulary of standard guarantee levels -- the basic guarantee, the strong guarantee, and the no-throw guarantee -- that let you state precisely what a function promises when an exception occurs partway through it. This page works through all three levels, what exception-neutral code means, why RAII is the mechanism that makes any of these guarantees achievable at all, the copy-and-swap idiom for getting the strong guarantee on assignment operators, and why a move constructor's noexcept-ness silently changes what std::vector does during reallocation.

What 'Exception Safe' Actually Means

A function is exposed to exceptions the moment it does anything that might throw -- allocating memory with new, calling another function that might throw, indexing with .at(), or calling a callback whose behavior isn't fully known. Once any of that is true, exception safety asks: if that operation throws right in the middle of this function's work, what state does the function leave its objects in? Answering 'I don't know' or 'whatever happens to be there' is exactly the situation exception safety guarantees are meant to rule out.

If other.log_.push_back(amount) throws (a real possibility -- push_back can need to allocate memory), step 1 has already happened but step 3 never will. balance_ has been debited from the source account, but the destination account never received the funds and never logged anything. That's a genuinely corrupted, inconsistent state -- money has vanished -- caused entirely by not thinking about what happens if the middle step throws.

class Account
{
public:
    void transferTo(Account &other, double amount)
    {
        balance_ -= amount;          // step 1
        other.log_.push_back(amount); // step 2 -- what if this throws?
        other.balance_ += amount;     // step 3 -- never reached if step 2 throws
    }

private:
    double balance_ = 0.0;
    std::vector<double> log_;
};

The Basic Guarantee

The basic guarantee is the weakest useful level: if an operation throws, no resources are leaked and the object's invariants still hold -- it's still a valid, usable object of its type -- but its exact contents afterward are unspecified. You can safely destroy the object or reassign it, but you can't assume anything about what value it currently holds.

If the third push_back throws while appending five values, the first two have already succeeded and stay in the vector -- v is left in a perfectly valid state (still a real, well-formed vector you can use normally), but its size and contents are not what the caller originally asked for. That's the basic guarantee: valid, but not necessarily what you expected.

#include <vector>

void appendSeveral(std::vector<int> &v, std::initializer_list<int> values)
{
    for (int value : values)
        v.push_back(value);   // if a later push_back throws (e.g. bad_alloc),
                               // earlier successful push_back calls are NOT undone
}

The Strong Guarantee

The strong guarantee is stricter: if the operation throws, it has no observable effect at all -- the object is left exactly as it was before the call started, as if the call had never happened. This is often called commit-or-rollback semantics, borrowing the database term: either the whole operation commits successfully, or it rolls back completely, with no partial states ever visible.

All the risky work -- copying and inserting -- happens on a local copy, never touching v directly. Only the very last step, v.swap(copy), actually changes v, and std::vector::swap is guaranteed not to throw (it just exchanges a few internal pointers). So either everything up to the swap succeeds and v ends up fully updated, or something throws before the swap and v is completely untouched -- there's no in-between state v can ever be observed in.

#include <vector>

std::vector<int>::iterator insertSorted(std::vector<int> &v, int value)
{
    std::vector<int> copy = v;                 // work on a separate copy
    auto pos = std::lower_bound(copy.begin(), copy.end(), value);
    copy.insert(pos, value);                   // if this throws, 'copy' is discarded and 'v' is untouched
    v.swap(copy);                               // swap() cannot throw -- this is the commit step
    return std::lower_bound(v.begin(), v.end(), value);
}

The No-Throw Guarantee

The no-throw guarantee is the strongest level: the operation is certified never to throw, period, under any input or circumstance. This is what noexcept, discussed on the Exception Handling page, is used to declare. Destructors, swap functions, move constructors, and move assignment operators are the classic candidates -- and for good reason, since other exception-safe code (like the copy-and-swap example above) often depends on specific operations being no-throw in order to build the strong guarantee out of them.

Swapping two raw pointers and two integers cannot fail -- there's no allocation, no I/O, nothing that could throw. Marking swap noexcept documents that guarantee explicitly and lets other code (like insertSorted's v.swap(copy) above) rely on it as a safe, unconditional commit step.

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

private:
    int *data_ = nullptr;
    std::size_t size_ = 0;
};

Exception-Neutral Code

Most functions in a well-written codebase don't try to catch every exception that might occur inside them -- they simply don't know how to fix a std::bad_alloc or a network timeout, so trying to catch it would either be a no-op or, worse, would hide the failure from code further up that does know what to do. Exception-neutral code lets exceptions it can't meaningfully handle propagate straight through it unchanged, while still remaining exception safe -- meaning its own local resources are still cleaned up correctly via RAII on the way out, even though it never wrote a single catch block itself.

processAll never catches anything -- if parseWidget throws on the third input, the exception propagates straight out of processAll to whichever caller is prepared to handle a parse failure. That's exception-neutral: processAll doesn't swallow, misinterpret, or paper over the exception. It's still exception safe, though, because widgets is a RAII container (std::vector of std::unique_ptr) -- every Widget successfully constructed before the throw still gets destroyed correctly during unwinding, with no leaks.

void processAll(const std::vector<std::string> &inputs)
{
    std::vector<std::unique_ptr<Widget>> widgets;
    for (const auto &input : inputs)
        widgets.push_back(parseWidget(input));   // parseWidget might throw -- and that's fine

    renderAll(widgets);
}

RAII: The Mechanism Behind Every Guarantee

None of the guarantee levels above are achievable through willpower or careful manual bookkeeping alone at scale -- they're achievable because RAII types (containers, smart pointers, lock guards) tie resource ownership to object lifetime, and object lifetime is exactly what stack unwinding manages automatically. The copy-and-swap idiom in the strong-guarantee example only worked because std::vector's own destructor and swap are already exception safe; build on top of well-behaved RAII types and your own code inherits their safety almost for free.

A useful mental shortcut: prefer building operations out of things that already provide the guarantee you want (a RAII wrapper's destructor, a container's already-safe swap, a smart pointer's reset) rather than trying to hand-write correct exception-safe logic out of raw resources and manual cleanup on every path. The fewer raw resources your own code touches directly, the fewer places you have to reason about by hand.

Transactional Updates and Copy-and-Swap

Copy-and-swap is the standard idiom for giving a class's assignment operator the strong guarantee, and it generalizes the insertSorted() pattern from earlier into a reusable technique: build the new state as an independent, fully-formed temporary; if anything during that construction throws, the original object was never touched at all; only once the new state exists completely do you commit by swapping it into place with a no-throw swap.

operator= takes its parameter 'other' BY VALUE, which means the copy constructor already ran to build it before operator= even starts -- if that copy throws (e.g. new int[] fails), it throws before *this is touched at all.

Once inside the function body, 'other' is a complete, independent copy of the new value the caller wants to assign.

swap(*this, other) exchanges the internals -- this cannot throw, since it's only pointer and size_t swaps.

After the swap, *this holds the new data, and 'other' (a local variable) now holds what used to be *this's old data.

When the function returns, 'other' goes out of scope and its destructor frees the OLD data -- cleanup happens automatically, with no explicit delete[] written in operator= itself.

This single pattern gets you the strong guarantee for free: either the parameter copy succeeds and the swap commits the new state (which can't fail), or the copy throws and *this was never touched. There's no code path that leaves *this half-updated.

#include <algorithm>
#include <cstddef>

class Buffer
{
public:
    Buffer(std::size_t size) : size_(size), data_(new int[size]) {}

    Buffer(const Buffer &other) : size_(other.size_), data_(new int[other.size_])
    {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    Buffer &operator=(Buffer other)   // note: parameter taken BY VALUE
    {
        swap(*this, other);          // commit step -- cannot throw
        return *this;
    }                                 // 'other' (holding the OLD state) is destroyed here

    ~Buffer() { delete[] data_; }

    friend void swap(Buffer &a, Buffer &b) noexcept
    {
        std::swap(a.size_, b.size_);
        std::swap(a.data_, b.data_);
    }

private:
    std::size_t size_;
    int *data_;
};

noexcept Move Operations and Why Containers Care

std::vector's reallocation logic has to move (or copy) every existing element into a new, larger buffer whenever it grows past capacity. If it used move for this and a move partway through the reallocation threw, the vector would be stuck in an impossible spot -- some elements already moved to the new buffer, some still in the old one, and no way to safely finish or cleanly roll back, because the source elements the old buffer needs to keep are already partially emptied out. To guarantee it can always give at least the basic guarantee during reallocation, std::vector checks at compile time whether a type's move constructor is noexcept.

Both types have a move constructor, but only NoexceptMovable's is marked noexcept. When each vector reallocates to fit a second element, std::vector uses std::move_if_noexcept internally: for NoexceptMovable it trusts the noexcept move and actually moves, printing 'move'; for ThrowingMovable, since the move constructor could theoretically throw and leave the vector in a state it can't safely recover from, std::vector conservatively falls back to copying instead, printing 'copy' -- silently slower, with no compiler error or warning to tell you why.

This is one of the most common silent performance traps in C++: a class with an expensive-to-copy but cheap-to-move resource (like a big buffer) gets no benefit from having a move constructor at all if that move constructor isn't marked noexcept, because std::vector will quietly keep copying instead of moving during every reallocation. If a move constructor genuinely cannot throw, mark it noexcept explicitly.

What is the difference between the basic guarantee and the strong guarantee?

The basic guarantee promises that after an exception, the object has no leaked resources and its invariants still hold -- it's a valid object -- but its exact contents are unspecified. The strong guarantee is stricter: if the operation throws, the object is left exactly as it was before the call started, with no observable change at all, as if the call never happened.

Why is std::vector::swap guaranteed not to throw, and why does that matter for the copy-and-swap idiom?

swap only exchanges a handful of internal pointers and size/capacity values between the two vectors -- it never allocates memory or does anything else that could fail. Copy-and-swap relies on exactly this property: it does all the risky, potentially-throwing work (like copying) on a separate temporary object, then uses a guaranteed-non-throwing swap as the single 'commit' step, which is what makes the whole operation provide the strong guarantee.

What does it mean for code to be 'exception-neutral,' and why is that different from being exception safe?

Exception-neutral code lets exceptions it doesn't know how to handle propagate through it unchanged, rather than catching and swallowing or misrepresenting them. It's still expected to be exception safe in the sense that its own local resources are cleaned up correctly (typically via RAII) while that exception passes through -- exception-neutral describes how a function treats exceptions it receives, while exception safe describes the state it leaves its own objects in.

Why does std::vector fall back to copying elements during reallocation instead of moving them, for a type whose move constructor is not marked noexcept?

If a move partway through reallocation threw, the vector could end up with some elements already moved into the new buffer and others still (partially disturbed) in the old one, with no safe way to finish or fully roll back. To avoid that, std::vector only trusts a type's move constructor during reallocation if it's marked noexcept; otherwise it conservatively copies elements instead, since a copy leaves the original elements untouched if it fails partway through.

In the copy-and-swap idiom, why does the assignment operator take its parameter by value instead of by reference?

Taking the parameter by value forces the copy constructor to run before the body of operator= even begins, building a complete, independent copy of the new value first. If that copy throws, it happens entirely outside of and before any modification to *this, so *this is guaranteed untouched. If the parameter were taken by reference instead, the copying would have to happen explicitly inside the function body, and getting the ordering right by hand becomes exactly the kind of manual bookkeeping copy-and-swap is meant to avoid.

Implement a Stack class (backed by std::vector<int>) with a push method that provides the strong exception-safety guarantee -- if pushing fails for any reason, the stack must be left exactly as it was before the call. Demonstrate it in main().

The key insight is that Stack::push doesn't need to implement the strong guarantee by hand at all -- std::vector::push_back already provides it: if a reallocation is needed and the allocation throws, the vector is guaranteed to remain exactly as it was before the call, with the old buffer untouched. Building on an already-exception-safe primitive (the vector) means Stack inherits the strong guarantee for free, which is the whole point of preferring RAII-based standard containers over hand-rolled buffer management.

This program's second emplace_back() triggers a reallocation. Does it move or copy the existing Wrapper into the new buffer, and why?

It copies, printing 'copy' -- even though Wrapper does have a move constructor. The move constructor here is not marked noexcept, so std::vector cannot safely trust that a move won't throw partway through, and it conservatively falls back to copying elements during reallocation instead. Marking the move constructor 'noexcept' (which would be accurate here, since it only reassigns pointers) would make the vector actually move instead.

Write a Config class holding a std::string name and a std::vector<int> values, with a copy-and-swap-based operator= that provides the strong exception-safety guarantee, and demonstrate assigning one Config to another in main().

operator= takes its parameter by value, so assigning b to a first copy-constructs a full independent copy of b (if that copy throws, a is never touched), then swap(*this, other) -- built entirely out of noexcept std::swap calls on a std::string and a std::vector -- exchanges the internals in a step that cannot fail. The output shows a printing 'defaults: 1 2 3' before the assignment and 'overrides: 9 8 7' after, confirming the copy-and-swap correctly replaced a's entire state.

Write a short program demonstrating the difference between std::vector's basic guarantee during ordinary push_back calls: build a vector, push several values, and explain in a comment above main() what state the vector would be left in (basic guarantee) if a later push_back's internal allocation threw partway through a longer sequence of pushes.

std::vector::push_back only offers the basic guarantee (not the strong guarantee) with respect to the whole sequence of calls: each individual push_back call itself is strongly exception safe (it either fully succeeds or leaves the vector completely unchanged), but a sequence of several push_back calls where a later one throws will leave the vector containing whatever elements were successfully pushed before the failure -- a valid vector, just a shorter one than intended. This distinguishes single-operation strong guarantees from guarantees about a whole multi-step sequence.

#include <iostream>
#include <vector>

struct NoexceptMovable
{
    NoexceptMovable() = default;
    NoexceptMovable(NoexceptMovable &&) noexcept { std::cout << "move\n"; }
    NoexceptMovable(const NoexceptMovable &) { std::cout << "copy\n"; }
};

struct ThrowingMovable
{
    ThrowingMovable() = default;
    ThrowingMovable(ThrowingMovable &&) { std::cout << "move\n"; }   // NOT noexcept
    ThrowingMovable(const ThrowingMovable &) { std::cout << "copy\n"; }
};

int main()
{
    std::cout << "-- NoexceptMovable --\n";
    std::vector<NoexceptMovable> a(1);
    a.reserve(1);
    a.emplace_back();   // triggers reallocation -- uses move (safe: noexcept)

    std::cout << "-- ThrowingMovable --\n";
    std::vector<ThrowingMovable> b(1);
    b.reserve(1);
    b.emplace_back();   // triggers reallocation -- falls back to COPY (move isn't noexcept)
}
struct Wrapper
{
    Wrapper(Wrapper&& other) { data = other.data; other.data = nullptr; }
    Wrapper(const Wrapper&) { std::cout << "copy\n"; }
    int* data = new int(5);
};

int main()
{
    std::vector<Wrapper> v;
    v.reserve(1);
    v.emplace_back();
    v.emplace_back();   // triggers reallocation
}