๐จ Why Error Handling Matters
Phase 9's capstone already used throw without explanation. This phase covers the real, structured tools C++ offers for handling things going genuinely wrong.
Imagine a function dividing two numbers, and the caller passes zero as the divisor. Without a real error-handling mechanism, the function either crashes the entire program, silently returns a meaningless value, or corrupts data โ none of which give the calling code a real, structured chance to notice the problem and respond sensibly.
| Strategy | How It Works |
|---|---|
| Return codes / bool | The function returns a special value (or a bool) indicating success/failure โ Phase 5's withdraw() capstone already used exactly this approach |
| Exceptions | The function throws an object describing the error, which propagates up until something explicitly catches it โ this phase's main focus |
Both are genuinely valid, real strategies used throughout professional C++ โ Module 6 covers exactly when each one fits better.
BankAccount::withdraw() returning bool to signal success or failure is precisely the "return codes" strategy โ you understood this pattern conceptually before this phase gave it a name and a formal alternative.๐ฏ try/catch/throw โ The Basics
The actual syntax behind Phase 9's throw std::runtime_error(...) โ how to both raise an exception and actually respond to one.
double divide(double a, double b) { if (b == 0) { throw std::runtime_error("Division by zero"); } return a / b; } int main() { try { double result = divide(10, 0); std::cout << result; } catch (const std::runtime_error& e) { std::cout << "Error: " << e.what() << std::endl; } return 0; }
| Keyword | Meaning |
|---|---|
throw | Raises an exception, immediately stopping normal execution and searching for a matching catch |
try | Marks a block of code where an exception might occur, that you want to watch for |
catch | Handles a specific type of exception if one is thrown inside the corresponding try block |
std::runtime_error (and the other standard exception types, Module 3) provide a .what() method returning a description of what went wrong โ "Division by zero" in this example, exactly the message passed to the constructor when the exception was created.
๐ Exception Types & std::exception
std::runtime_error isn't the only option โ a whole family of standard exception types exist for different kinds of problems, all built on Phase 6's inheritance.
Every standard exception type inherits from a common base, std::exception โ directly applying Phase 6's inheritance concepts. This is exactly why .what() works consistently across all of them: it's declared once, virtually, in the shared base class.
| Type | Typically Used For |
|---|---|
std::runtime_error | General errors detected only while the program is running |
std::logic_error | Errors that reflect a genuine bug in the program's logic, theoretically detectable before running |
std::invalid_argument | A function received an argument that doesn't make sense |
std::out_of_range | Accessing something (like an index) outside a valid range |
class InsufficientFundsError : public std::runtime_error { public: InsufficientFundsError() : std::runtime_error("Insufficient funds") {} }; throw InsufficientFundsError();
You can create your OWN exception type by inheriting from a standard one โ genuinely just Phase 6 Module 3's derived-class-constructor pattern, applied to build a domain-specific error type that's still fully compatible with catch (const std::runtime_error&) and .what().
๐ Multiple catch Blocks
A single try can be followed by several catch blocks, each handling a genuinely different kind of problem differently.
try { processOrder(orderId); } catch (const std::invalid_argument& e) { std::cout << "Bad input: " << e.what() << std::endl; } catch (const std::runtime_error& e) { std::cout << "Runtime problem: " << e.what() << std::endl; } catch (...) { std::cout << "Something else entirely went wrong." << std::endl; }
C++ checks each catch block IN ORDER and uses the first one whose type matches โ genuinely similar in spirit to Phase 1 Module 8's else-if chain. catch (...) is a genuine catch-all, matching literally anything not caught by an earlier, more specific block.
std::invalid_argument IS-A std::runtime_error's relative in the exception hierarchy, if you put the more general std::runtime_error catch FIRST, it would catch invalid_argument exceptions too โ the more specific block would never actually run. Always order catch blocks from most specific to most general, exactly the same principle as Phase 6's polymorphism relies on.๐ก๏ธ RAII & Exception Safety
Phase 8's RAII principle turns out to be genuinely essential once exceptions enter the picture โ here's exactly why.
void processData() { int* data = new int[1000]; riskyOperation(); // throws an exception! delete[] data; // NEVER REACHED โ the exception skipped right past this line }
When an exception is thrown, C++ immediately unwinds the stack, skipping any remaining code in the current function โ including a manual delete that would otherwise have run. This is a genuine, real memory leak (Phase 4 Module 7), specifically caused by exceptions interacting badly with manual memory management.
void processData() { std::unique_ptr<int[]> data = std::make_unique<int[]>(1000); riskyOperation(); // throws โ but data's destructor STILL runs during stack unwinding! } // even though this line is never reached, data was already safely cleaned up
Because Phase 8's smart pointers tie cleanup to the object's own destructor, and destructors are GUARANTEED to run during stack unwinding (as the Pre-Course and Phase 5 first established), data is genuinely cleaned up correctly โ even though the function never reaches its last line. This is exactly why professional C++ code overwhelmingly prefers RAII/smart pointers over manual new/delete.
๐ฆ When NOT to Use Exceptions
Exceptions are genuinely powerful, but Module 1's other strategy โ return codes โ is often the actually better choice. Here's how to decide.
| Situation | Better Fit | Why |
|---|---|---|
| A file genuinely doesn't exist, disk fails, network drops | Exception | Genuinely unexpected โ the normal flow of the program has no sensible way to just continue |
| A user types invalid input in a form | Return code / bool | Entirely expected, ordinary, routine โ happens constantly in real, working programs |
| Looking up a key that might not be in a map | Return code (like Phase 7's .find() returning .end()) | A missing key is a completely normal, expected outcome, not exceptional at all |
Throwing and catching an exception carries real runtime overhead, well beyond a simple bool check or comparison. Using exceptions for routine, EXPECTED situations โ like validating everyday user input โ is both a performance concern and, often, a design smell: exceptions are meant for genuinely exceptional circumstances, not ordinary control flow.
๐ Capstone โ A Validating Parser
Every idea from this phase, combined: a custom exception type, multiple catch blocks, and a deliberate mix of exceptions and return codes, applying Module 6's judgment directly.
Build a function that parses a string as an age (a whole number between 0 and 130), throwing a custom exception for genuinely malformed input, while a separate, simpler function checks whether a parsed age qualifies as "senior" (65+) using a plain bool โ deliberately applying both strategies where each genuinely fits.
#include <iostream> #include <stdexcept> #include <string> class InvalidAgeError : public std::invalid_argument { public: InvalidAgeError(std::string reason) : std::invalid_argument(reason) {} }; int parseAge(std::string input) { try { int age = std::stoi(input); // throws std::invalid_argument if input isn't a number at all if (age < 0 || age > 130) { throw InvalidAgeError("Age must be between 0 and 130"); } return age; } catch (const std::invalid_argument&) { throw InvalidAgeError("'" + input + "' is not a valid number"); } } bool isSenior(int age) { // an ordinary, expected check โ a bool return is the right fit here return age >= 65; } int main() { std::string inputs[] = {"42", "abc", "200"}; for (const auto& input : inputs) { try { int age = parseAge(input); std::cout << input << " -> valid age " << age << ", senior: " << (isSenior(age) ? "yes" : "no") << std::endl; } catch (const InvalidAgeError& e) { std::cout << input << " -> invalid: " << e.what() << std::endl; } } return 0; }
InvalidAgeError(Module 3) provides a genuinely meaningful, specific exception type rather than a generic one.parseAgecatches the STL's ownstd::invalid_argumentfromstd::stoiand re-throws it as the more specificInvalidAgeErrorโ a real, common pattern for translating a library's generic errors into your own domain-specific ones.isSeniordeliberately uses a plainbool, not an exception โ an age simply not being 65+ is entirely ordinary, expected behavior (Module 6's judgment call, applied directly).- The loop's single
try/catchhandles every genuinely malformed input without crashing the whole program.
Add Negative-Number Detection Before stoi
Currently, "-5" would parse successfully via std::stoi and only get caught by the range check. Add an explicit, early check for a leading - character, throwing a more SPECIFIC message ("Age cannot be negative") before the generic range check runs โ practicing Module 4's principle of ordering more specific handling before general handling.