Skip to main content

    Lesson 16 • Advanced Track

    Exception Safety

    By the end of this lesson you'll be able to throw and catch exceptions correctly, let RAII clean up automatically when something fails, and choose the right safety guarantee — basic, strong, or no-throw — for every function you write.

    What You'll Learn

    • Throw, try, and catch exceptions the idiomatic way
    • Use std::exception and std::runtime_error correctly
    • Understand stack unwinding and why RAII makes code leak-free
    • Name the three guarantees: basic, strong, and no-throw
    • Provide the strong guarantee with the copy-and-swap idiom
    • Mark functions noexcept and know when it actually matters

    💡 Real-World Analogy

    Think of a function call as a person climbing a ladder, one rung per call. A throw is shouting "abort!" and sliding back down. As you pass each rung on the way down, you must put back anything you borrowed there — that automatic clean-up is stack unwinding, and the destructors are the "put it back" step. The three guarantees describe how tidy you leave the room when you bail out: basic = nothing dropped on the floor (no leaks); strong = the room looks exactly as it did before you entered (all-or-nothing); no-throw = you promise you will never need to abort at all.

    1. Throw, Try, and Catch

    An exception is an error object that travels up the call stack until something handles it. You throw it to signal "I can't continue", wrap risky code in a try block, and handle the problem in a catch. Standard error types live in <stdexcept>: std::runtime_error for problems found at run time and std::invalid_argument for bad inputs — both derive from std::exception, whose .what() method returns your message. Read this worked example, then run it.

    Worked example: throw / try / catch

    Read every comment, run it, and check the output matches.

    Try it Yourself »
    C++
    #include <iostream>
    #include <stdexcept>   // std::runtime_error, std::invalid_argument
    using namespace std;
    
    // Withdraw money — throw if the request is impossible.
    double withdraw(double balance, double amount) {
        if (amount <= 0)
            // throw STOPS the function and hands an error object up the stack.
            throw invalid_argument("Amount must be positive");
        if (amount > balance)
            throw runtime_error("Insufficient funds");
        return balance - amount;   // normal (happy) pat
    ...

    Notice how the line after the failing call was skipped — a throw jumps straight to the matching catch. Now your turn: fill in the three blanks to write your own throw, try, and catch.

    🎯 Your turn: divide safely

    Fill in the ___ blanks, then check your output against the expected lines.

    Try it Yourself »
    C++
    #include <iostream>
    #include <stdexcept>
    using namespace std;
    
    int safeDivide(int a, int b) {
        // 🎯 YOUR TURN — fill in each ___ then press "Try it Yourself".
    
        // 1) If b is 0, throw a runtime_error with a message.
        if (b == 0)
            ___;                       // 👉 throw runtime_error("Cannot divide by zero")
    
        return a / b;
    }
    
    int main() {
        // 2) Wrap the risky call in a try block.
        ___ {                          // 👉 the keyword that starts a guarded block
            cout 
    ...

    2. Stack Unwinding & Why RAII Wins

    When a throw fires, C++ unwinds the stack: it walks back up through every function it was inside and destroys each local object along the way, running its destructor. This is the secret to exception safety — if your resources (memory, files, locks, connections) are owned by local objects, they are guaranteed to be released, even on the error path. That pattern is RAII (Resource Acquisition Is Initialisation): tie a resource's lifetime to an object's lifetime, and you never have to remember to clean up manually.

    Worked example: RAII cleans up during unwinding

    Watch the destructor run before the catch — no leak, no manual cleanup.

    Try it Yourself »
    C++
    #include <iostream>
    #include <memory>      // std::unique_ptr
    #include <stdexcept>
    using namespace std;
    
    struct Connection {
        Connection()  { cout << "  Connection OPENED" << endl; }
        ~Connection() { cout << "  Connection CLOSED" << endl; } // runs no matter what
        void send()   { throw runtime_error("network dropped"); }
    };
    
    void doWork() {
        // RAII: the resource lives in a local object. When doWork() exits —
        // normally OR by a thrown exception — the destructor runs automaticall
    ...

    3. The Three Exception-Safety Guarantees

    Every function offers one of three promises about what happens when it throws. The basic guarantee says: no leaks, the object stays usable, but its state may have partly changed. The strong guarantee says: all-or-nothing — if it throws, nothing changed. The no-throw guarantee says: this can never fail, so you mark it noexcept. This example shows all three on one class.

    Worked example: basic, strong, and no-throw

    One class demonstrating all three guarantees side by side.

    Try it Yourself »
    C++
    #include <iostream>
    #include <vector>
    #include <stdexcept>
    using namespace std;
    
    class Account {
        vector<string> history;   // a record of every action
    public:
        // BASIC guarantee: no leaks, object stays valid — but state MAY change.
        void logBasic(const string& note) {
            if (note.empty())
                throw invalid_argument("empty note");
            history.push_back(note);  // if this throws, no leak; vector still valid
        }
    
        // STRONG guarantee: all-or-nothing. On throw, nothi
    ...

    4. The Copy-and-Swap Idiom (Strong Guarantee)

    The classic recipe for the strong guarantee is copy-and-swap: do all the risky work on a copy, and only when it has fully succeeded, swap the copy into place. Because std::swap on standard containers is noexcept (it just exchanges internal pointers), the commit step can't fail — so either everything works or your original is untouched. Your turn: complete the two blanks to make addAll strong.

    🎯 Your turn: make it all-or-nothing

    Copy first, mutate the copy, then swap to commit. Fill in the blanks.

    Try it Yourself »
    C++
    #include <iostream>
    #include <vector>
    #include <stdexcept>
    using namespace std;
    
    class Playlist {
        vector<string> songs;
    public:
        // Add several songs as ALL-OR-NOTHING (the strong guarantee).
        void addAll(const vector<string>& newSongs) {
            // 🎯 YOUR TURN — make this strong with copy-and-swap.
    
            // 1) Make a COPY of songs to do the risky work on.
            vector<string> temp = ___;     // 👉 copy the current songs vector
    
            for (const auto& s : newSongs) {
              
    ...

    5. noexcept and Why It Matters

    noexcept is a promise to the compiler that a function never throws. Break that promise and the program calls std::terminate immediately — so only mark things that truly can't fail (moves, swaps, simple getters). The biggest payoff is performance: std::vector will only move its elements when it grows if their move constructor is noexcept; otherwise it must copy them to preserve its own strong guarantee.

    Common mistake: forgetting noexcept on a move constructor silently makes vector fall back to copying — a quiet performance killer for types with expensive copies.

    Worked example: noexcept moves

    See how noexcept lets vector move (cheap) instead of copy on regrowth.

    Try it Yourself »
    C++
    #include <iostream>
    #include <vector>
    #include <utility>
    using namespace std;
    
    class Buffer {
        int* data;
        size_t sz;
    public:
        Buffer(size_t n) : data(new int[n]{}), sz(n) {}
        ~Buffer() { delete[] data; }                 // RAII: frees memory
    
        // noexcept move ctor — vector PREFERS this when it grows. Without
        // noexcept the vector must COPY instead (to keep the strong guarantee).
        Buffer(Buffer&& other) noexcept
            : data(other.data), sz(other.sz) {
            other.dat
    ...

    Common Errors (and the fix)

    • Leaking on throw (raw new with no RAII): int* p = new int[n]; risky(); leaks if risky() throws, because the delete[] never runs. Fix: own the resource with std::unique_ptr / std::vector so unwinding frees it for you.
    • Catching by value (slicing): catch (std::exception e) copies and slices the object — you lose the derived type and often the real message. Fix: always catch (const std::exception& e).
    • Throwing from a destructor: destructors are noexcept by default, so a thrown exception calls std::terminate and crashes. Fix: wrap failable cleanup in a try/catch inside the destructor and handle it there.
    • Swallowing exceptions: an empty catch (...) { } hides real bugs and leaves the program in an unknown state. Fix: handle it, log it, or rethrow with throw; — never silently ignore it.
    • terminate called after throwing an instance of 'std::runtime_error': an exception escaped main with no matching catch. Fix: add a try/catch (const std::exception& e) around the call that can throw.

    📋 Quick Reference — the three guarantees

    GuaranteePromise on throwHow you provide it
    BasicNo leaks; object still valid, but state may have changedRAII (own resources in objects)
    StrongAll-or-nothing; state is exactly as before the callCopy-and-swap
    No-throwNever throws at allnoexcept (moves, swap, getters)

    Aim for the strongest guarantee you can afford. The strong guarantee costs a copy, so use it for critical operations and the basic guarantee on hot paths.

    Frequently Asked Questions

    Q: Why catch by const reference (const std::exception&) instead of by value?

    Catching by value copies the exception, and if you catch the base type std::exception by value you slice off the derived part — losing the real message and type. Catching by const reference keeps the full object and avoids the copy, so it is the standard idiom.

    Q: What is stack unwinding?

    When an exception is thrown, C++ walks back up the call stack looking for a matching catch, destroying every fully-constructed local object on the way. Those destructors are what free your resources — which is exactly why RAII makes code exception-safe automatically.

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

    The basic guarantee promises no leaks and a valid object, but the state may have partially changed. The strong guarantee promises all-or-nothing: if the operation throws, the object is exactly as it was before the call. Copy-and-swap is the usual way to provide the strong guarantee.

    Q: Should I ever throw from a destructor?

    No. Destructors are implicitly noexcept since C++11, so an exception escaping one calls std::terminate and crashes the program — and during stack unwinding a second exception is undefined behaviour. Do any failable cleanup inside the destructor in a try/catch and swallow it there.

    Q: Why does noexcept matter for performance?

    std::vector only moves your objects when it reallocates if the move constructor is noexcept; otherwise it copies them to preserve the strong guarantee. Marking moves noexcept can turn an expensive copy of every element into a cheap pointer steal.

    Mini-Challenge: a Strong-Guarantee Cart

    No blanks this time — just a brief and an outline. Make addItems all-or-nothing with copy-and-swap and mark total as noexcept. Run it and confirm the rejected batch leaves the cart unchanged.

    🎯 Mini-Challenge: build a strong-guarantee cart

    Provide the strong guarantee with copy-and-swap, then add a noexcept total().

    Try it Yourself »
    C++
    #include <iostream>
    #include <vector>
    #include <stdexcept>
    using namespace std;
    
    class Cart {
        vector<double> prices;
    public:
        // 🎯 MINI-CHALLENGE: a strong-guarantee "checkout"
        // 1. addItems(items): give the STRONG guarantee using copy-and-swap.
        //      - copy prices into a temp vector
        //      - if any price is negative, throw invalid_argument("bad price")
        //      - otherwise push it onto the temp
        //      - swap(prices, temp) only at the very end
        // 2. total(): m
    ...

    Pro Tips

    • 💡 Let RAII do the cleanup. If every resource is owned by an object, you almost never need a try/catch just to free things.
    • 💡 Catch by const&, throw by value. Throw a temporary, catch a reference to the base type — it avoids slicing and copies.
    • 💡 Mark moves and swaps noexcept. It unlocks the fast path in standard containers and signals intent.
    • 💡 Pick the cheapest guarantee that's correct. Strong for critical updates, basic everywhere the copy would hurt.

    🎉 Lesson Complete

    • throw raises an error object; try guards risky code; catch handles it
    • ✅ Standard errors (std::runtime_error, std::invalid_argument) derive from std::exception — catch by const&
    • ✅ A throw unwinds the stack, and RAII destructors free resources automatically — no leaks
    • ✅ Three guarantees: basic (no leaks), strong (all-or-nothing), no-throw (noexcept)
    • Copy-and-swap gives the strong guarantee; noexcept moves keep containers fast
    • Next lesson: Design Patterns — reusable solutions to common C++ design problems

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service