TeqStudy ยท C++ Track ยท Phase 10

Error Handling

try/catch/throw, the standard exception hierarchy, and the crucial connection between exceptions and Phase 8's RAII โ€” plus knowing when a return code is actually the better tool.

๐Ÿšจ ๐ŸŽฏ ๐Ÿ“‹ ๐Ÿ›ก๏ธ ๐Ÿ
Course Progress
0%
MODULE 01

๐Ÿšจ 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.

๐Ÿ˜– What Happens Without Real Error Handling

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.

๐Ÿ› ๏ธ Two Genuine Strategies
StrategyHow It Works
Return codes / boolThe function returns a special value (or a bool) indicating success/failure โ€” Phase 5's withdraw() capstone already used exactly this approach
ExceptionsThe 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.

๐Ÿ’ก
You've technically already used a return-code-style approachPhase 5's 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.
๐Ÿง  Quick Check
What are the two genuine strategies C++ offers for handling errors?
MODULE 02

๐ŸŽฏ 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.

๐ŸŽฏ The Three Keywords
C++
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;
}
KeywordMeaning
throwRaises an exception, immediately stopping normal execution and searching for a matching catch
tryMarks a block of code where an exception might occur, that you want to watch for
catchHandles a specific type of exception if one is thrown inside the corresponding try block
๐Ÿ“ž e.what() โ€” Getting the Error Message

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.

๐Ÿง  Quick Check
When a throw statement executes, what happens immediately?
MODULE 03

๐Ÿ“‹ 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.

๐ŸŒณ A Genuine Inheritance Hierarchy

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.

TypeTypically Used For
std::runtime_errorGeneral errors detected only while the program is running
std::logic_errorErrors that reflect a genuine bug in the program's logic, theoretically detectable before running
std::invalid_argumentA function received an argument that doesn't make sense
std::out_of_rangeAccessing something (like an index) outside a valid range
๐Ÿงฌ Custom Exception Types
C++
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().

๐Ÿง  Quick Check
Why does .what() work consistently across every standard exception type?
MODULE 04

๐Ÿ”€ Multiple catch Blocks

A single try can be followed by several catch blocks, each handling a genuinely different kind of problem differently.

๐Ÿ”€ Catching Different Types Differently
C++
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.

๐Ÿ“ A Real Ordering Rule โ€” Specific Before General
โš ๏ธ
Order genuinely matters, connecting directly to Module 3's inheritanceSince 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.
๐Ÿง  Quick Check
Why must a more specific exception type's catch block be placed BEFORE a more general one?
MODULE 05

๐Ÿ›ก๏ธ RAII & Exception Safety

Phase 8's RAII principle turns out to be genuinely essential once exceptions enter the picture โ€” here's exactly why.

๐Ÿ˜ฑ The Danger โ€” A Manual delete That Never Runs
C++ โ€” a genuine memory leak
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.

โœ… RAII Fixes This Automatically
C++ โ€” genuinely exception-safe
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.

๐Ÿ’ก
This connects three entire phases into one coherent picturePhase 4's manual memory management, Phase 8's RAII/smart pointers, and this phase's exceptions all meet exactly here โ€” RAII isn't just "more convenient" than manual delete, it's genuinely the only reliable way to guarantee cleanup once exceptions are part of your program.
๐Ÿง  Quick Check
Why does a manual delete statement risk being skipped entirely if an exception is thrown earlier in the same function?
MODULE 06

๐Ÿšฆ 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.

โš–๏ธ The Real Distinction โ€” Exceptional vs. Expected
SituationBetter FitWhy
A file genuinely doesn't exist, disk fails, network dropsExceptionGenuinely unexpected โ€” the normal flow of the program has no sensible way to just continue
A user types invalid input in a formReturn code / boolEntirely expected, ordinary, routine โ€” happens constantly in real, working programs
Looking up a key that might not be in a mapReturn code (like Phase 7's .find() returning .end())A missing key is a completely normal, expected outcome, not exceptional at all
โšก A Genuine Performance Consideration

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.

๐Ÿ’ก
A useful rule of thumbIf a calling function would reasonably be expected to handle a specific failure as part of its NORMAL logic (like "did the user enter a valid number?"), a return code usually fits better. If the failure represents something that breaks the program's normal assumptions entirely (like "the file we need is genuinely gone"), an exception usually fits better.
๐Ÿง  Quick Check
Why might a return code (like a bool) be a better fit than an exception for validating everyday user input?
MODULE 07 ๐Ÿ

๐Ÿ 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.

๐ŸŽฏ The Goal

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.

๐Ÿ’ป A Reference Solution
main.cpp
#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;
}
๐Ÿ” Why It's Built This Way
  • InvalidAgeError (Module 3) provides a genuinely meaningful, specific exception type rather than a generic one.
  • parseAge catches the STL's own std::invalid_argument from std::stoi and re-throws it as the more specific InvalidAgeError โ€” a real, common pattern for translating a library's generic errors into your own domain-specific ones.
  • isSenior deliberately uses a plain bool, 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/catch handles every genuinely malformed input without crashing the whole program.
๐Ÿ Extend It Yourself

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.

Roadmap