A tightrope walker high above a city. Photo · Alfonso Betancourt / Unsplash
Things will go wrong. The interesting question is what your program does when they do.

Up to now your code has lived in the happy case: file opens, allocation succeeds, math doesn't overflow. Real programs spend a sizeable fraction of their lives outside that happy case — and what they do there determines whether they're trustworthy software or a thing that crashes once a month. C++ gives you two main tools for handling errors. The community has spent thirty years arguing about which is better. There is no settled answer. There are good defaults.

Mechanism 1: exceptions (try / catch / throw)

double divide(double a, double b) {
    if (b == 0) throw std::invalid_argument("divide by zero");
    return a / b;
}

try {
    double x = divide(10, 0);
    use(x);                  // never runs
} catch (const std::exception& e) {
    std::cerr << "caught: " << e.what();
}

When throw fires, control jumps up the stack until it lands in a catch block whose type matches. Along the way, every object on the stack has its destructor run — the magic from Week 24. RAII guarantees that file handles close, mutexes release, and memory is freed even on the error path. Without RAII, exceptions would be a disaster; with it, they're elegant.

Exceptions are good for: rare, exceptional conditions where you can't reasonably continue. File-not-found at startup. Out-of-memory. Programmer-error invariants violated. Things you'd want to log, propagate up, and handle a few stack frames later.

Mechanism 2: return-value error codes

The other approach: functions return a value indicating success or failure, plus the actual answer through a pointer parameter or a tagged union. C does this everywhere (look at fopen returning NULL on failure, errno being set). Modern C++ has a clean version using std::optional<T> (C++17) or std::expected<T, E> (C++23):

std::optional<double> divide(double a, double b) {
    if (b == 0) return std::nullopt;
    return a / b;
}

if (auto x = divide(10, 0)) {
    use(*x);
} else {
    std::cerr << "divide failed\n";
}

The function makes its possible failure visible in the type system. Callers cannot ignore it — they must check for "no value" before using the result. Rust's Result<T, E> is the same idea taken to its logical conclusion; Go's multiple return values (value, err) is a cousin.

Return-code style is good for: errors that are expected in the normal course of business. Network packet not yet arrived, database row not found, user typed an invalid input. Things callers should be checking for and reacting to, not propagating up.

Which to use

Reasonable defaults, after thirty years of accumulated industry experience:

Some C++ codebases ban exceptions entirely (Google's old style guide; embedded systems where the runtime support isn't available; game engines where the stack-unwinding cost matters). Those projects use error codes everywhere. Other projects (Qt, most of the standard library, most "normal" application code) use exceptions liberally. Both can be made to work; consistency within a project matters more than which religion you pick.

The cost of exceptions

Modern compilers implement exceptions with zero runtime cost on the happy path — the throw machinery uses lookup tables consulted only when an exception fires. So try { … } catch (…) is free if no exception happens. Throwing one is expensive — much slower than a normal return. The rule: exceptions for exceptional cases. If half your function calls throw, you've designed wrong.

Exception size also matters: every catch site adds compile-time bookkeeping; very deep call chains with deeply-nested try/catch can balloon binary size. Real, but rarely the bottleneck.

Why this matters for AI

PyTorch throws c10::Error exceptions when shapes don't match, devices conflict, or CUDA fails. The Python side translates these into Python exceptions you actually see (RuntimeError: ... CUDA out of memory). The C++ side relies on RAII to ensure that, when those exceptions fire, every CUDA buffer that was about to be allocated is released, every stream is closed, every reference is decremented. Without RAII the failure mode would be a hung GPU.

If you've ever caught a CUDA OOM in Python and your next tensor allocation works fine, you've experienced exception safety done right.

Errors that you can ignore are errors that will eventually bite. Make them visible.

Try it yourself

What's next

One more piece of practical C++ before we close Phase 3 — the surprisingly intricate question of how to organise a multi-file project. C++ gets this from C, and gets it half right, in interesting ways.

Week 28 is Headers & Implementation.

Photo credit

Photo free under the Unsplash license. Tightrope · Alfonso Betancourt.