🧬 Phase 5 · Templates & Advanced 🟡 Intermediate MODULE 21

Exception Handling

⏱️ 22 min read
📖 Theory + Code
🧩 5 Quiz Questions
🏗️ 1 Challenge
Your progress in Phase 550%
🎯 What you'll learn: Why exceptions exist, the try/catch/throw syntax, throwing built-in exception types from <stdexcept>, catching by const reference, chaining multiple catch blocks, and a real worked example — a divide function that fails safely instead of crashing.

Why Exceptions Exist

Not every runtime error can — or should — be handled the moment it happens. Dividing by zero, opening a file that doesn't exist, or receiving invalid input are all situations your program needs a plan for. Without one, the program either produces garbage results or crashes outright.

Exceptions are C++'s mechanism for signaling that something went wrong, separate from a function's normal return value. Instead of a function returning a special error code that every caller must remember to check, it can throw an exception object that travels up the call stack until some surrounding code catches it and decides what to do.

💡
Error codes vs. exceptions
A function returning -1 to mean "failed" relies on every caller remembering to check it — easy to forget. An exception cannot be silently ignored: if nothing catches it, the program terminates with a clear error message, which is far safer than quietly continuing with bad data.
🚨
throw
Signals that something went wrong and hands off an exception object describing it.
🛡️
try
Wraps code that might fail, so any exception it throws can be caught nearby.
🧯
catch
Receives the thrown exception and runs recovery code instead of crashing.
📚
<stdexcept>
Standard header providing ready-made exception types like runtime_error and invalid_argument.

try, catch & throw

The pattern has three parts: a try block containing code that might fail, a throw statement inside some function that raises the problem, and a catch block that runs if that exception occurs. Built-in exception types like std::runtime_error live in <stdexcept> and store a human-readable message you can retrieve with .what().

divide_safe.cpp
C++
#include <iostream>
#include <stdexcept>
using namespace std;

double divide(double a, double b) {
    if (b == 0) {
        throw runtime_error("Division by zero!");
    }
    return a / b;
}

int main() {
    try {
        cout << "10 / 2 = " << divide(10, 2) << "\n";
        cout << "5 / 0 = "  << divide(5, 0) << "\n";  // throws here
        cout << "This line never runs\n";
    } catch (const exception& e) {
        cout << "Error: " << e.what() << "\n";
    }
    cout << "Program continues normally after the catch block\n";
    return 0;
}

Notice the flow: divide(10, 2) succeeds and prints normally. divide(5, 0) throws — execution immediately jumps out of the try block (skipping the "This line never runs" print) straight to the matching catch. Once the catch block finishes, the program keeps running normally — it did not crash.

Always catch by const reference
catch (const std::exception& e) avoids copying the exception object and, more importantly, correctly catches derived exception types too, since C++ exceptions participate in the same inheritance rules as regular classes. Catching by value can slice a derived exception down to its base type.

Built-in Exception Types

Rather than always throwing a generic runtime_error, <stdexcept> provides several more specific types so callers can tell what actually went wrong. Two of the most common:

⚠️
std::invalid_argument
Thrown when an argument's value doesn't make sense for the operation — e.g. a negative index.
🧨
std::runtime_error
A general-purpose error detected only while the program is running — division by zero, a failed operation.
📏
std::out_of_range
Thrown when accessing an index or position outside a valid range — like an array bound.
🌳
std::exception
The common base class every standard exception type inherits from — useful as a catch-all.

All of these derive from std::exception, and all support .what() to describe the failure. Choosing the more specific type when you throw makes it possible for callers to react differently depending on exactly what failed.

Multiple catch Blocks

A single try can be followed by several catch blocks, each handling a different exception type. C++ checks them in order, top to bottom, and runs the first one that matches. A good pattern is to list the most specific exception types first, and put a catch (const std::exception& e) last as a fallback for anything you didn't anticipate.

multi_catch.cpp
C++
#include <iostream>
#include <stdexcept>
#include <vector>
using namespace std;

int getElement(const vector<int>& v, int index) {
    if (index < 0)
        throw invalid_argument("Index cannot be negative");
    if (index >= (int)v.size())
        throw out_of_range("Index is beyond vector bounds");
    return v[index];
}

int main() {
    vector<int> nums = {10, 20, 30};
    int indices[] = {1, -1, 10};

    for (int idx : indices) {
        try {
            cout << "nums[" << idx << "] = " << getElement(nums, idx) << "\n";
        } catch (const invalid_argument& e) {
            cout << "Invalid argument: " << e.what() << "\n";
        } catch (const out_of_range& e) {
            cout << "Out of range: " << e.what() << "\n";
        } catch (const exception& e) {
            cout << "Unexpected error: " << e.what() << "\n";
        }
    }
    return 0;
}

Output: nums[1] = 20, then Invalid argument: Index cannot be negative, then Out of range: Index is beyond vector bounds. Each iteration's error is caught by the specific block that matches it — the generic catch (const exception&) never even runs here, since both thrown types were handled first.

⚠️
Order matters — most specific first
Since invalid_argument and out_of_range both derive from std::exception, if you put catch (const exception& e) first, it would catch everything and the more specific blocks below it would never run. The compiler won't stop you from writing unreachable catch blocks — order them deliberately.
🧩 Knowledge Check — Lesson 21
Answer all 5 questions to test your understanding. Instant feedback on every answer.
1. What is the main purpose of exceptions in C++?
2. Which keyword raises (signals) an exception?
3. Why should you catch exceptions by const reference, e.g. catch (const std::exception& e)?
4. You have catch blocks for invalid_argument, out_of_range, and exception. In what order should they appear?
5. What does e.what() return on a caught std::exception?
💪
Coding Challenge — Lesson 21
Apply what you learned · Intermediate Level

Now write real exception-safe code. Complete the challenge below in your local compiler or IDE.

Challenge: Validate an Age Input 🎂

Write a function int validateAge(int age) that:

• throws std::invalid_argument if age is negative
• throws std::out_of_range if age is greater than 130
• otherwise returns age unchanged

In main(), test it inside a try/catch with at least three values: one valid age, one negative age, and one age over 130. Print a clear message for each case, and make sure the program never crashes.
💡 Show hints if you're stuck
  • #include <stdexcept> gives you both exception types
  • Check the negative case first, then the too-large case
  • Use two catch blocks: one for invalid_argument, one for out_of_range
  • Call your function multiple times inside separate try blocks, or loop over a small array of test ages
Finished this lesson?
Mark it complete to track your progress.
🎉

Lesson 21 Complete!

Your programs can now fail gracefully instead of crashing. Next: reading and writing real files with fstream.

Module 21 of 26 Phase 5 — Templates & Advanced Concepts