AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

15 Errors and exceptions / Lesson 2 of 5

Exception safety — guarantees, noexcept and RAII

14 min · +10 XP · 2 exercises · 7 questions

Read the text14 min
Exercises0/2
Quiz7 q

Quick answer

What are the exception safety guarantees in C++?

C++ functions give one of three exception safety guarantees. No-throw: the operation cannot fail, as with destructors and swap. Strong: if it throws, the state is exactly as before — commit or roll back, as with std::vector::push_back. Basic: if it throws, nothing leaks and every invariant holds, but the state may have changed. Anything weaker, such as a leak or a broken invariant, is a bug.

An exception that passes through a function is a return the function did not plan. What does the caller find afterwards — the old state, a new one, or half of each with a file handle leaked on the way? Exception safety is the discipline of answering that question for every function you write, and C++ makes the good answers cheap if you let objects own resources. This lesson settles the three guarantees, how RAII delivers the basic one for free, what noexcept promises and why std::vector reads it, why a destructor must never throw, and when a return value is the better tool.

The three guarantees

Dave Abrahams named them while building the STL, and every standard library function documents which it gives:

GuaranteeIf the operation throws …Examples
No-throwit cannotdestructors, swap, std::move of a well-designed type, size()
Strongthe state is exactly as before — commit or roll backstd::vector::push_back, std::map::insert of one element
Basicno leak and every invariant holds, but the state may have changedstd::vector::assign, most multi-step updates
nonea leak, a half-built object, a broken invarianta bug

The order is deliberate: no-throw is strongest and rarest, basic is the floor. Consider a transfer between two ledgers:

void transfer(Ledger& from, Ledger& to, long long amount) {
    from.withdraw(amount);   // may throw InsufficientFunds: nothing has changed yet — fine
    to.deposit(amount);      // may throw (say, an audit log allocation fails): the money has left `from`
}

If deposit throws, both ledgers are valid objects and nothing leaked — the basic guarantee — but the money is gone. Making it strong means doing every step that can fail before the first step that changes anything, or being able to undo.

RAII makes the basic guarantee automatic

Stack unwinding runs destructors (lesson 1). So a resource held by an object whose destructor releases it is released on every exit path, including the ones you did not write:

void writeReport(const std::vector<Row>& rows, std::mutex& m) {
    std::ofstream out("report.txt");            // closed by ~ofstream
    std::lock_guard<std::mutex> lock(m);        // unlocked by ~lock_guard
    auto buffer = std::make_unique<char[]>(4096);   // freed by ~unique_ptr
    for (const auto& r : rows) format(out, r, buffer.get());   // may throw: all three are still released
}

The raw form — FILE* f = fopen(...), m.lock(), new char[4096] — leaks all three the moment format throws, because the fclose, unlock and delete[] lines are never reached. Module 7, lesson 3 introduced RAII as a lifetime tool; this is its other half. The rule: never hold a resource in a raw variable that a throw could skip. Resources belong to objects.

No finally: the scope guard

Java and JavaScript run cleanup in finally. C++ deliberately has none, because a destructor generalises it — the cleanup is attached to the thing, not repeated at every call site. When the cleanup is a one-off action rather than a resource, wrap it in a guard:

template <typename F>
class ScopeExit {
public:
    explicit ScopeExit(F f) : f_(std::move(f)) {}
    ~ScopeExit() { if (active_) f_(); }
    void release() noexcept { active_ = false; }   // commit: the cleanup is no longer wanted
    ScopeExit(const ScopeExit&) = delete;
    ScopeExit& operator=(const ScopeExit&) = delete;
private:
    F f_;
    bool active_ = true;
};

void update(Ledger& ledger, const Batch& batch) {
    Ledger backup = ledger;
    ScopeExit rollback([&] { ledger = std::move(backup); });   // runs on any exit ...
    for (const auto& entry : batch) ledger.apply(entry);       // ... including a throw from here
    rollback.release();                                        // ... unless every apply succeeded
}

Class template argument deduction names the lambda's type for you. The guard turns a basic-guarantee loop into a strong one: on a throw, the destructor restores the backup during unwinding; on success, release() disarms it.

The strong guarantee: risky work first, commit last

The general recipe needs no guard at all — do everything that can throw on a temporary, then commit with operations that cannot throw:

void Inventory::load(const std::string& path) {
    std::map<std::string, int> fresh = parse(path);   // every allocation and every parse error happens here
    items_.swap(fresh);                               // swap is no-throw: the commit cannot fail
}

If parse throws, items_ was never touched. Copy-and-swap assignment (Module 9, lesson 2) is this recipe applied to operator=. The price is a temporary — a full copy for a large object — which is why the standard library gives the strong guarantee where it is cheap and documents basic where it is not. Say which you give in the function's comment.

noexcept

void f() noexcept; declares that f will not let an exception escape. It is a promise, not a check: the compiler will happily compile a throw inside f, and when one gets out, std::terminate is called with no unwinding to the caller. The specifier can be conditional — noexcept(std::is_nothrow_move_constructible_v<T>) — and the operator form noexcept(expr) asks whether an expression could throw without evaluating it: noexcept(v.at(0)) is false, noexcept(v.size()) is true.

Some functions are noexcept without saying so. Destructors are implicitly noexcept unless a member's destructor is not; defaulted special members (Module 9, lesson 4) are noexcept when every member's corresponding operation is. So a class whose members are std::string and std::vector gets a noexcept defaulted move for free, and a class with a user-written move constructor gets nothing unless you write the word.

Why std::vector reads noexcept

When a vector grows, it must relocate every existing element into the new buffer. Moving them is fast, but if a move throws halfway, the old buffer is already partly gutted and the strong guarantee push_back promises is impossible. Copying is safe — the old buffer stays intact until the last copy succeeds — but slow. The library resolves this with std::move_if_noexcept(elem), which yields T&& when T's move constructor is noexcept (or T has no copy constructor at all) and const T& otherwise. So a type whose move constructor is noexcept is moved; a type whose move constructor may throw is copied on every reallocation, silently, forever. Declare move constructors and move assignment noexcept. The instrumented exercise makes the decision visible.

Never throw from a destructor

Destructors run during unwinding. If one throws while an exception is already in flight, two exceptions exist at once and std::terminate is called — there is no way to handle both. Since C++11 destructors are implicitly noexcept, so a throwing destructor terminates even outside unwinding. The design consequence: a cleanup that can genuinely fail — flushing a file, committing a database transaction — gets an explicit member function that may throw, and the destructor calls a swallowing version:

class Connection {
public:
    void close() { if (open_) { open_ = false; sendClose(); } }   // may throw: callers who care call this
    ~Connection() {
        try { close(); } catch (...) { /* log at most; never rethrow */ }
    }
private:
    bool open_ = true;
    void sendClose();
};

std::fstream does the same: its destructor closes and swallows any failure, and a program that needs to know calls close() and checks the stream.

Exceptions or return values?

SituationPrefer
The caller expects the failure often — a lookup miss, user input that fails to parsea return value: std::optional or an error code (lessons 3 and 5)
The failure is rare and the immediate caller cannot fix itan exception
A constructor cannot establish its invariantan exception — constructors have no return value
Operators, callbacks, deep call chains with no channel for a codean exception
A hot loop, or a codebase built with -fno-exceptionsa return value
Destructors, swap, movesneither — they must not fail

Pitfalls

MistakeWhat happens
new/lock()/fopen in a raw variableLeaked on any throw before the release line
A user-written move constructor without noexceptstd::vector copies on every reallocation
A destructor that throwsstd::terminate, immediately
Changing state before the last step that can throwBasic guarantee where the caller assumed strong
Marking a function noexcept that calls at() or allocatesA throw terminates instead of propagating

Key takeaways

  • No-throw, strong, basic: know which one each function gives and say so.
  • RAII plus unwinding gives the basic guarantee for free; a raw resource in a local is a leak waiting for a throw.
  • Strong guarantee recipe: do the throwing work on a temporary, commit with swap or a move; a scope guard undoes on failure.
  • noexcept is a promise enforced by std::terminate; destructors have it implicitly.
  • std::vector moves on reallocation only when the move is noexcept — write the word on every move constructor.
  • Never let a destructor throw; give failing cleanup an explicit close().

Common questions

What does noexcept mean in C++?

noexcept declares that a function will not let an exception escape. It is a promise, not a check: if an exception does escape, std::terminate is called. Destructors are implicitly noexcept, and the operator form noexcept(expr) asks at compile time whether an expression could throw, without evaluating it.

Why should move constructors be noexcept?

When a std::vector reallocates, it uses std::move_if_noexcept: it moves the elements only if the move constructor is noexcept, or the type cannot be copied, and copies them otherwise, to keep the strong guarantee of push_back. A move constructor without noexcept therefore makes every reallocation copy, silently.

Can a destructor throw an exception in C++?

It must not let one escape. Destructors are implicitly noexcept since C++11, so a throwing destructor calls std::terminate, and a throw during stack unwinding would leave two exceptions in flight. Put cleanup that can fail in an explicit function such as close(), and have the destructor call it inside try { … } catch (...) { }.

How does RAII make C++ code exception safe?

RAII ties each resource to an object whose destructor releases it, and stack unwinding runs destructors on every exit path, including exceptions. A std::ofstream, std::lock_guard or std::unique_ptr is therefore released when a later line throws, while a raw fopen, lock() or new leaks. RAII gives the basic guarantee automatically.

How do you give a function the strong exception guarantee?

Do all the work that can throw on a temporary, then commit with operations that cannot throw: build a fresh map, then items_.swap(fresh). If anything throws, the original was never touched. Copy-and-swap assignment is this recipe, and a scope guard that restores a backup on exit is the alternative.

Mark as readSign in to keep your place.

C++ Exception Safety Guarantees, noexcept and RAII

The basic, strong and no-throw exception safety guarantees in C++, how RAII gives the basic one for free, what noexcept promises and why std::vector checks it.

  • Course: C++ study plan
  • Module: Errors and exceptions
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: Clang 18 · C++20

What are the exception safety guarantees in C++?

C++ functions give one of three exception safety guarantees. No-throw: the operation cannot fail, as with destructors and swap. Strong: if it throws, the state is exactly as before — commit or roll back, as with std::vector::push_back. Basic: if it throws, nothing leaks and every invariant holds, but the state may have changed. Anything weaker, such as a leak or a broken invariant, is a bug.

Lesson

An exception that passes through a function is a return the function did not plan. What does the caller find afterwards — the old state, a new one, or half of each with a file handle leaked on the way? Exception safety is the discipline of answering that question for every function you write, and C++ makes the good answers cheap if you let objects own resources. This lesson settles the three guarantees, how RAII delivers the basic one for free, what noexcept promises and why std::vector reads it, why a destructor must never throw, and when a return value is the better tool.

The three guarantees

Dave Abrahams named them while building the STL, and every standard library function documents which it gives:

GuaranteeIf the operation throws …Examples
No-throwit cannotdestructors, swap, std::move of a well-designed type, size()
Strongthe state is exactly as before — commit or roll backstd::vector::push_back, std::map::insert of one element
Basicno leak and every invariant holds, but the state may have changedstd::vector::assign, most multi-step updates
nonea leak, a half-built object, a broken invarianta bug

The order is deliberate: no-throw is strongest and rarest, basic is the floor. Consider a transfer between two ledgers:

void transfer(Ledger& from, Ledger& to, long long amount) {
    from.withdraw(amount);   // may throw InsufficientFunds: nothing has changed yet — fine
    to.deposit(amount);      // may throw (say, an audit log allocation fails): the money has left `from`
}

If deposit throws, both ledgers are valid objects and nothing leaked — the basic guarantee — but the money is gone. Making it strong means doing every step that can fail before the first step that changes anything, or being able to undo.

RAII makes the basic guarantee automatic

Stack unwinding runs destructors (lesson 1). So a resource held by an object whose destructor releases it is released on every exit path, including the ones you did not write:

void writeReport(const std::vector<Row>& rows, std::mutex& m) {
    std::ofstream out("report.txt");            // closed by ~ofstream
    std::lock_guard<std::mutex> lock(m);        // unlocked by ~lock_guard
    auto buffer = std::make_unique<char[]>(4096);   // freed by ~unique_ptr
    for (const auto& r : rows) format(out, r, buffer.get());   // may throw: all three are still released
}

The raw form — FILE* f = fopen(...), m.lock(), new char[4096] — leaks all three the moment format throws, because the fclose, unlock and delete[] lines are never reached. Module 7, lesson 3 introduced RAII as a lifetime tool; this is its other half. The rule: never hold a resource in a raw variable that a throw could skip. Resources belong to objects.

No finally: the scope guard

Java and JavaScript run cleanup in finally. C++ deliberately has none, because a destructor generalises it — the cleanup is attached to the thing, not repeated at every call site. When the cleanup is a one-off action rather than a resource, wrap it in a guard:

template <typename F>
class ScopeExit {
public:
    explicit ScopeExit(F f) : f_(std::move(f)) {}
    ~ScopeExit() { if (active_) f_(); }
    void release() noexcept { active_ = false; }   // commit: the cleanup is no longer wanted
    ScopeExit(const ScopeExit&) = delete;
    ScopeExit& operator=(const ScopeExit&) = delete;
private:
    F f_;
    bool active_ = true;
};

void update(Ledger& ledger, const Batch& batch) {
    Ledger backup = ledger;
    ScopeExit rollback([&] { ledger = std::move(backup); });   // runs on any exit ...
    for (const auto& entry : batch) ledger.apply(entry);       // ... including a throw from here
    rollback.release();                                        // ... unless every apply succeeded
}

Class template argument deduction names the lambda's type for you. The guard turns a basic-guarantee loop into a strong one: on a throw, the destructor restores the backup during unwinding; on success, release() disarms it.

The strong guarantee: risky work first, commit last

The general recipe needs no guard at all — do everything that can throw on a temporary, then commit with operations that cannot throw:

void Inventory::load(const std::string& path) {
    std::map<std::string, int> fresh = parse(path);   // every allocation and every parse error happens here
    items_.swap(fresh);                               // swap is no-throw: the commit cannot fail
}

If parse throws, items_ was never touched. Copy-and-swap assignment (Module 9, lesson 2) is this recipe applied to operator=. The price is a temporary — a full copy for a large object — which is why the standard library gives the strong guarantee where it is cheap and documents basic where it is not. Say which you give in the function's comment.

noexcept

void f() noexcept; declares that f will not let an exception escape. It is a promise, not a check: the compiler will happily compile a throw inside f, and when one gets out, std::terminate is called with no unwinding to the caller. The specifier can be conditional — noexcept(std::is_nothrow_move_constructible_v<T>) — and the operator form noexcept(expr) asks whether an expression could throw without evaluating it: noexcept(v.at(0)) is false, noexcept(v.size()) is true.

Some functions are noexcept without saying so. Destructors are implicitly noexcept unless a member's destructor is not; defaulted special members (Module 9, lesson 4) are noexcept when every member's corresponding operation is. So a class whose members are std::string and std::vector gets a noexcept defaulted move for free, and a class with a user-written move constructor gets nothing unless you write the word.

Why std::vector reads noexcept

When a vector grows, it must relocate every existing element into the new buffer. Moving them is fast, but if a move throws halfway, the old buffer is already partly gutted and the strong guarantee push_back promises is impossible. Copying is safe — the old buffer stays intact until the last copy succeeds — but slow. The library resolves this with std::move_if_noexcept(elem), which yields T&& when T's move constructor is noexcept (or T has no copy constructor at all) and const T& otherwise. So a type whose move constructor is noexcept is moved; a type whose move constructor may throw is copied on every reallocation, silently, forever. Declare move constructors and move assignment noexcept. The instrumented exercise makes the decision visible.

Never throw from a destructor

Destructors run during unwinding. If one throws while an exception is already in flight, two exceptions exist at once and std::terminate is called — there is no way to handle both. Since C++11 destructors are implicitly noexcept, so a throwing destructor terminates even outside unwinding. The design consequence: a cleanup that can genuinely fail — flushing a file, committing a database transaction — gets an explicit member function that may throw, and the destructor calls a swallowing version:

class Connection {
public:
    void close() { if (open_) { open_ = false; sendClose(); } }   // may throw: callers who care call this
    ~Connection() {
        try { close(); } catch (...) { /* log at most; never rethrow */ }
    }
private:
    bool open_ = true;
    void sendClose();
};

std::fstream does the same: its destructor closes and swallows any failure, and a program that needs to know calls close() and checks the stream.

Exceptions or return values?

SituationPrefer
The caller expects the failure often — a lookup miss, user input that fails to parsea return value: std::optional or an error code (lessons 3 and 5)
The failure is rare and the immediate caller cannot fix itan exception
A constructor cannot establish its invariantan exception — constructors have no return value
Operators, callbacks, deep call chains with no channel for a codean exception
A hot loop, or a codebase built with -fno-exceptionsa return value
Destructors, swap, movesneither — they must not fail

Pitfalls

MistakeWhat happens
new/lock()/fopen in a raw variableLeaked on any throw before the release line
A user-written move constructor without noexceptstd::vector copies on every reallocation
A destructor that throwsstd::terminate, immediately
Changing state before the last step that can throwBasic guarantee where the caller assumed strong
Marking a function noexcept that calls at() or allocatesA throw terminates instead of propagating

Key takeaways

  • No-throw, strong, basic: know which one each function gives and say so.
  • RAII plus unwinding gives the basic guarantee for free; a raw resource in a local is a leak waiting for a throw.
  • Strong guarantee recipe: do the throwing work on a temporary, commit with swap or a move; a scope guard undoes on failure.
  • noexcept is a promise enforced by std::terminate; destructors have it implicitly.
  • std::vector moves on reallocation only when the move is noexcept — write the word on every move constructor.
  • Never let a destructor throw; give failing cleanup an explicit close().

Common questions

What does noexcept mean in C++?

noexcept declares that a function will not let an exception escape. It is a promise, not a check: if an exception does escape, std::terminate is called. Destructors are implicitly noexcept, and the operator form noexcept(expr) asks at compile time whether an expression could throw, without evaluating it.

Why should move constructors be noexcept?

When a std::vector reallocates, it uses std::move_if_noexcept: it moves the elements only if the move constructor is noexcept, or the type cannot be copied, and copies them otherwise, to keep the strong guarantee of push_back. A move constructor without noexcept therefore makes every reallocation copy, silently.

Can a destructor throw an exception in C++?

It must not let one escape. Destructors are implicitly noexcept since C++11, so a throwing destructor calls std::terminate, and a throw during stack unwinding would leave two exceptions in flight. Put cleanup that can fail in an explicit function such as close(), and have the destructor call it inside try { … } catch (...) { }.

How does RAII make C++ code exception safe?

RAII ties each resource to an object whose destructor releases it, and stack unwinding runs destructors on every exit path, including exceptions. A std::ofstream, std::lock_guard or std::unique_ptr is therefore released when a later line throws, while a raw fopen, lock() or new leaks. RAII gives the basic guarantee automatically.

How do you give a function the strong exception guarantee?

Do all the work that can throw on a temporary, then commit with operations that cannot throw: build a fresh map, then items_.swap(fresh). If anything throws, the original was never touched. Copy-and-swap assignment is this recipe, and a scope guard that restores a backup on exit is the alternative.

Exercises

Released on every path

Prove that unwinding runs destructors, in order, before the handler. Write Guard, an RAII class whose constructor prints acquire <name> and whose destructor prints release <name> (copying deleted). Then run each job through a recursive run(const Job&, std::size_t i) that declares a Guard for resource i and recurses for i + 1, so every resource is held in its own frame. When all resources are held, the job either prints work <name> and returns, or throws std::runtime_error("step failed in <name>"). main catches per job. Do not write a release line by hand anywhere: the destructors produce every one of them, on the success path and on the exception path alike.

Input: n, then n lines <name> <k> <resource-1> … <resource-k> <ok|fail>. Output: the acquire/work/release trace as it happens, then <name>: ok or <name>: failed: <what()> per job.

3
backup 2 db file ok
sync 1 net fail
noop 0 ok

prints

acquire db
acquire file
work backup
release file
release db
backup: ok
acquire net
release net
sync: failed: step failed in sync
work noop
noop: ok

Moved or copied?

Make the noexcept decision visible. Probe (given) records which constructor built it: copied or moved. Define four types, each holding a std::string label and a Probe probe and constructible from a string:

  • Defaulted — declares no copy or move at all (rule of zero);
  • Safe — a user-written copy constructor and a user-written move constructor marked noexcept, each forwarding to the members' corresponding constructor;
  • Risky — the same two constructors, but the move constructor without noexcept;
  • CopyOnly — a user-written copy constructor and no move constructor.

relocate<T> (given) builds a T, relocates it with T target(std::move_if_noexcept(source)) — exactly what std::vector does to each element on reallocation — and prints <label>: nothrow move <yes|no>, <moved|copied>, the first word from std::is_nothrow_move_constructible_v<T> and the second from target.probe.how.

Input: n, then n lines <kind> <label> where kind is defaulted, safe, risky or copyonly. Output: one line per object, then moved <a>, copied <b>.

4
defaulted d
safe s
risky r
copyonly c

prints

d: nothrow move yes, moved
s: nothrow move yes, moved
r: nothrow move no, copied
c: nothrow move no, copied
moved 2, copied 2

In this module: Errors and exceptions

← Exceptions — throw, try, catch and the standard hierarchy · std::optional — a value that may be absent →