Skip to content

Bank 11 — Exceptions

Eleven exercises on making failure part of the design rather than a special case. Module 13. Levels L1–L6.

Solutions are in Solutions and answers; nudges are in Hints. Attempt first.


Exercises

E117 — Why throw at all? L1 concept

Task. Give two concrete reasons exceptions are better than returning an error code for a constructor that cannot build a valid object. Then say what throw does to the caller's normal control flow, and what guarantee a correctly written program gets when an exception leaves a scope.


E118 — Caught in the right order L2 predict

#include <iostream>
#include <stdexcept>

int main() {
    try {
        throw std::runtime_error("boom");
    }
    catch (const std::logic_error&) {
        std::cout << "logic\n";
    }
    catch (const std::runtime_error& e) {
        std::cout << "runtime: " << e.what() << '\n';
    }
    std::cout << "after\n";
}

Task. Write the output. Then: what would happen if the two catch clauses were swapped, and why? (std::runtime_error and std::logic_error both derive from std::exception.)


E119 — A negative amount is a failure L2 complete

#include <iostream>
#include <stdexcept>
#include <string>

// TODO (1): define an exception type AmountError, deriving from std::runtime_error,
//           whose constructor takes a message.

class Wallet {
public:
    void deposit(double amount) {
        // TODO (2): throw AmountError when amount is not positive
        balance_ += amount;
    }
    double balance() const { return balance_; }
private:
    double balance_ = 0.0;
};

int main() {
    Wallet w;
    w.deposit(50.0);
    try {
        w.deposit(-5.0);
    }
    catch (const AmountError& e) {
        std::cout << "rejected: " << e.what() << '\n';
    }
    std::cout << w.balance() << '\n';
}

Task. Write AmountError and the check so the program prints a rejection line and then 50. Why should the exception derive from std::runtime_error rather than from nothing?


E120 — Inner and outer L2 trace scenario

A payment service validates, then charges. Inner code decides whether to rethrow.

#include <iostream>
#include <stdexcept>

void charge(double amount) {
    if (amount > 100.0)
        throw std::out_of_range("limit exceeded");
    std::cout << "charged " << amount << '\n';
}

int main() {
    try {
        std::cout << "begin\n";
        try {
            charge(250.0);
            std::cout << "unreachable\n";
        }
        catch (const std::out_of_range& e) {
            std::cout << "inner saw " << e.what() << '\n';
            throw;                       // rethrow the same exception
        }
    }
    catch (const std::exception& e) {
        std::cout << "outer saw " << e.what() << '\n';
    }
    std::cout << "end\n";
}

Task. Write the output in order. What does the bare throw; do, and what would the output be if it were replaced by throw std::runtime_error("wrapped");?


E121 — Validate on the way in L3 modify scenario

A reservation system accepts bookings without checking them, and bad data reaches the rest of the program.

#include <iostream>
#include <string>
#include <stdexcept>

class Reservation {
public:
    Reservation(std::string name, int seats) : name_(name), seats_(seats) {}
    std::string name()  const { return name_; }
    int         seats() const { return seats_; }
private:
    std::string name_;
    int         seats_;
};

int main() {
    Reservation good{ "Aslam", 3 };
    Reservation bad{ "", -2 };                // nonsense, accepted
    std::cout << good.name() << ' ' << bad.seats() << '\n';
}

Task. Make the constructor reject an empty name and a non-positive seat count by throwing std::invalid_argument. Where exactly should the check go, and why is throwing from the constructor better than a bool ok() a caller might forget to call?


E122 — Caught by value L4 debug

#include <iostream>
#include <stdexcept>
#include <string>

class ValidationError : public std::runtime_error {
public:
    explicit ValidationError(std::string field)
        : std::runtime_error("invalid " + field), field_(std::move(field)) {}
    const std::string& field() const { return field_; }
private:
    std::string field_;
};

int main() {
    try {
        throw ValidationError{"email"};
    }
    catch (std::runtime_error e) {          // note: by value
        std::cout << "caught: " << e.what() << '\n';
        // e.field();                        // line F
    }
}

Task. Why does line F fail to compile even though we threw a ValidationError? Name the phenomenon, say what a copy by value does to the derived part, and give the correct way to write the catch so that field() is available.


E123 — A destructor that throws L4 predict

#include <iostream>
#include <stdexcept>

class Guard {
public:
    ~Guard() {
        std::cout << "closing ";
        throw std::runtime_error("cannot close");
    }
};

void work() {
    Guard g;
    throw std::runtime_error("work failed");     // starts unwinding
}

int main() {
    try {
        work();
    }
    catch (const std::exception& e) {
        std::cout << "caught " << e.what() << '\n';
    }
}

Task. Describe what happens when work() runs. Which exception reaches catch? Be precise about when g's destructor runs and what the program does at that moment. Then state the rule this demonstrates for destructors, and why destructors are usually marked noexcept.


E124 — The order of the catches L4 analyze

throw std::out_of_range("bad index");

Task. For each ordering below, say which handler runs (or that the code will not compile) and why:

  1. catch (std::exception&) before catch (std::out_of_range&);
  2. catch (std::out_of_range&) before catch (std::exception&);
  3. catch (...) before catch (std::out_of_range&);
  4. catch (std::out_of_range) (by value).

Task (continued). Then state the rule for the order in which catch clauses should be written, and why catching by const reference is the default choice.


E125 — Leaks on the throwing path L4 debug scenario

#include <iostream>
#include <stdexcept>
#include <string>

class Order {
public:
    Order(std::string id) : id_(std::move(id)), rows_(new int[10]) {}
    ~Order() { delete[] rows_; }
    const std::string& id() const { return id_; }
private:
    std::string id_;
    int*        rows_;
};

void process(int count) {
    Order first{"A-1"};
    Order* second = new Order{"B-2"};
    if (count > 5) {
        throw std::runtime_error("too many");    // second leaks here
    }
    delete second;
    std::cout << "processed " << first.id() << '\n';
}

Task. Which object leaks when process(9) runs, and why does the other one not leak even though no delete runs for it? Then fix the leak in the way that fits the course so far and explain why the fix is more robust than adding a delete before every throw.


E126 — Design an error hierarchy L5 design scenario

An order pipeline can fail in four distinct ways: the customer is unknown, an item is out of stock, a discount code is invalid, and the payment provider times out. Callers must be able to handle some of these together (for example, retry timeouts) while still being able to distinguish them, and every error must carry a human-readable message.

Task. Design the exception hierarchy. Give:

  1. the base exception type and what it derives from;
  2. each of the four concrete types and the extra information each carries;
  3. which errors a caller can catch together, and how your hierarchy makes that a single catch clause;
  4. one error you deliberately do not model as an exception, and why;
  5. the destructor/what() contract you rely on.

How you will be judged. On whether a caller can express "retry any timeout" and "handle any invalid order" with the catches your hierarchy allows.


E127 — Codes to exceptions L6 refactor scenario

A parser returns int error codes (0 ok, -1 bad input, -2 too long, -3 bad checksum) and callers check them inconsistently; one caller ignored the return value and shipped corrupted data.

Task. Write a refactoring plan that replaces the codes with exceptions. It must:

  1. map each code to a concrete exception type;
  2. say where the throw happens and where the catches go;
  3. address the caller that ignored the code — what changes for it;
  4. name one thing that becomes harder once failures throw, and how you would handle it;
  5. state how you would prove no call site still depends on the old int protocol.

Success. Failure is impossible to ignore silently, and the migration is complete rather than half-done.


Previous topic: Templates · Next topic: The STL with OOP · Hints: hints.md · Solutions: solutions.md