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++98A 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:
| Level | Guarantee | Typical mechanism |
|---|---|---|
| No-throw | Never throws; noexcept | Trivial ops, destructors, swaps |
| Strong | Succeeds or leaves state unchanged | Copy-and-swap, transactional update |
| Basic | No leaks, invariants hold, state may change | RAII, smart pointers |
| None | Anything goes on throw | Avoid |
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.
// 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
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.
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.
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)
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
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:
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.
| Level | What the caller can count on | Typical cost |
|---|---|---|
| No-throw | Function never propagates an exception; noexcept on the declaration | Restrict to operations that genuinely cannot fail |
| Strong | Succeeds completely or leaves state byte-for-byte unchanged (commit-or-rollback) | Extra copy or transactional staging area |
| Basic | No resource leaks, all invariants hold, but observable state may differ | RAII ownership — usually achievable for free |
| None | Anything can happen — leaks, partial writes, broken invariants | Legacy C-style code; avoid in new code |
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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 kind | Rule | Reason |
|---|---|---|
| Destructor | Always noexcept (implicit since C++11) | Throwing during unwinding calls std::terminate |
| Move constructor | noexcept when possible | std::vector reallocation moves only if noexcept |
| Move assignment | noexcept when possible | Same; also enables std::swap optimisations |
swap | Always noexcept | Standard algorithms and containers assume nothrow swap |
| Observer / getter | noexcept if body cannot throw | Documents intent; enables inlining optimisations |
noexcept(noexcept(expr)) | Generic wrappers | Propagates 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.
// 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 wrongSee Also
- RAII — the foundation of basic exception safety
- Move Semantics — why
noexcepton moves matters noexceptspecifier — syntax and conditionalnoexcept- Copy-and-Swap Idiom — canonical strong-guarantee assignment
- Error Handling — exceptions vs error codes vs
std::expected