Exception Handling
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.
-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.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().
#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.
const referencecatch (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:
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.
#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.
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.const reference, e.g. catch (const std::exception& e)?catch blocks for invalid_argument, out_of_range, and exception. In what order should they appear?e.what() return on a caught std::exception?Now write real exception-safe code. Complete the challenge below in your local compiler or IDE.
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 unchangedIn
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
catchblocks: one forinvalid_argument, one forout_of_range - Call your function multiple times inside separate
tryblocks, or loop over a small array of test ages