Skip to main content

Exception Safety and the Strong Guarantee

Exception safety is the contract a function makes about program state when an exception passes through it. The four levels form a ladder; most code should aim for the strong guarantee โ€” all-or-nothing โ€” and noexcept (the no-throw rung) is the tool that makes the commit step possible.

Scope split

This page is about the safety guarantees. For the noexcept specifier itself โ€” syntax, the noexcept operator, conditional noexcept, which functions to mark, performance โ€” see the canonical noexcept Specifier page.

The four levelsโ€‹

GuaranteePromise on exceptionTypical source
No guaranteeanything โ€” leaks, corruptionraw new/delete, no cleanup
Basicno leaks, object still valid (but maybe changed)partial update is acceptable
Strongstate is exactly as before โ€” operation rolled backcommit-or-rollback designs
No-thrownever throws at allnoexcept functions, swaps, dtors

The diagram is the strong guarantee in one picture: do the throwing work on a copy, then commit with an operation that cannot throw. If the work throws, the original is untouched.

Basic vs strong, concretelyโ€‹

// BASIC guarantee โ€” if push_back throws midway, some items are already in,
// count_ may disagree with data_. No leak, but state changed.
void addItems(const std::vector<int>& items) {
for (int x : items) { data_.push_back(x); ++count_; }
}

// STRONG guarantee โ€” all work happens on a copy; the only mutation of *this
// is the final noexcept move. Throw anywhere above and *this is unchanged.
void addItems(const std::vector<int>& items) {
std::vector<int> temp = data_; // copy (may throw)
temp.insert(temp.end(), items.begin(), items.end()); // (may throw)
data_ = std::move(temp); // commit (noexcept)
count_ += items.size();
}

This "prepare on a copy, commit with a non-throwing move/swap" shape is the core technique. The copy-and-swap idiom packages it for assignment operators; PIMPL does it by swapping a single pointer.

The commit step must be no-throw

The whole scheme collapses if the commit can throw. That is why move assignment and swap need to be noexcept โ€” and why the standard library only moves (instead of copying) elements during vector reallocation when the move constructor is noexcept. A throwing move would break the strong guarantee, so the library plays it safe and copies. See noexcept and move semantics.

Rollback when you can't prepare on a copyโ€‹

If copying the whole object is too expensive, achieve the strong guarantee by saving enough to undo:

void transactionalPush(int value) {
auto backup = data_; // save what we need to restore
try {
data_.push_back(value);
backup.clear(); // success โ€” drop the backup
} catch (...) {
data_ = std::move(backup); // rollback (noexcept move)
throw; // rethrow: caller sees the original state
}
}

Choosing a levelโ€‹

You do not always want the strong guarantee โ€” it has a cost (the copy). Pick deliberately:

  • No-throw for destructors, swaps, move operations, and simple observers โ€” and mark them noexcept.
  • Strong for operations a caller will retry or that must not corrupt shared state (assignment, bulk updates, transactions).
  • Basic when a copy is too expensive and the caller can cope with a valid-but-changed object.
Let the library do the work

Standard containers already provide strong/basic guarantees for their operations. The cheapest way to be exception-safe is to build from std::vector, std::string, and smart pointers, and let RAII handle every cleanup path โ€” then you rarely write a try/catch at all.

Worked example โ€” an exception-safe Stackโ€‹

Each method's guarantee is called out; note how the no-throw ones are noexcept and the mutating one leans on vector's own strong guarantee.

template <class T>
class Stack {
std::vector<T> data_;
public:
void push(const T& v) { data_.push_back(v); } // strong (vector's)
void pop() noexcept { if (!data_.empty()) data_.pop_back(); }
bool empty() const noexcept { return data_.empty(); }
size_t size() const noexcept { return data_.size(); }

T top() const { // strong: returns by value
if (data_.empty()) throw std::out_of_range("empty stack");
return data_.back();
}
};

Summaryโ€‹

  • Exception safety has four levels: no-guarantee, basic, strong, no-throw.
  • The strong guarantee = do throwing work on a copy, then commit with a no-throw move/swap.
  • That commit step is why move/swap must be noexcept โ€” and why containers copy instead of move when the move can throw.
  • Use rollback when a full copy is too costly; choose the level deliberately โ€” strong isn't free.
  • Build on RAII and standard containers; they hand you most of these guarantees for free.