Exceptions

Back

Loading concept...

🛡️ C++ Exceptions: Your Safety Net for Unexpected Problems


The Big Picture: What Are Exceptions?

Imagine you’re walking on a tightrope. Below you is a safety net. If you slip (something goes wrong), the net catches you so you don’t crash to the ground.

Exceptions in C++ work exactly like that safety net!

When your program does something risky—like dividing by zero, opening a missing file, or running out of memory—instead of crashing, it can “throw” a problem (exception). Somewhere else in your code, a “catch” block is waiting like a safety net to handle it gracefully.


🎯 What You’ll Learn

graph TD A["🎯 Exceptions"] --> B["try-catch Blocks"] A --> C["throw Statement"] A --> D["Standard Exception Classes"] A --> E["Custom Exceptions"] A --> F["noexcept Specification"] A --> G["Stack Unwinding"] A --> H["Exception Safety Guarantees"]

1️⃣ try-catch Blocks: Setting Up Your Safety Net

Think of try as saying “I’m about to do something risky” and catch as “If something goes wrong, here’s what to do.”

The Basic Pattern

try {
    // Risky code goes here
    // Like walking on a tightrope
} catch (exception_type& e) {
    // Handle the problem here
    // The safety net catches you!
}

Real Example: Dividing Numbers

#include <iostream>
#include <stdexcept>
using namespace std;

int divide(int a, int b) {
    if (b == 0) {
        throw runtime_error("Can't divide by zero!");
    }
    return a / b;
}

int main() {
    try {
        cout << divide(10, 2) << endl;  // Works: 5
        cout << divide(10, 0) << endl;  // Problem!
    } catch (runtime_error& e) {
        cout << "Oops! " << e.what() << endl;
    }
    return 0;
}

Output:

5
Oops! Can't divide by zero!

Catching Multiple Types

You can have multiple safety nets for different problems:

try {
    // risky code
} catch (runtime_error& e) {
    cout << "Runtime problem: " << e.what();
} catch (logic_error& e) {
    cout << "Logic problem: " << e.what();
} catch (...) {
    // This catches EVERYTHING else
    cout << "Unknown problem occurred!";
}

💡 Pro Tip: The catch(...) is like a giant safety net that catches anything. Use it as a last resort!


2️⃣ throw Statement: Sounding the Alarm

When you detect a problem, you throw an exception. It’s like yelling “HELP!” and trusting someone will catch you.

Simple Throw

throw runtime_error("Something broke!");

Throwing Different Things

// Throw a standard exception
throw invalid_argument("Age can't be negative");

// Throw a number (not recommended, but possible)
throw 404;

// Throw a string (also not recommended)
throw "File not found";

When Should You Throw?

Good reasons to throw:

  • Can’t complete the task you were asked to do
  • Input is invalid or unexpected
  • A critical resource is unavailable

Bad reasons to throw:

  • Normal conditions (like end of a list)
  • Things you could handle locally

Example: Validating Input

void setAge(int age) {
    if (age < 0) {
        throw invalid_argument("Age can't be negative!");
    }
    if (age > 150) {
        throw out_of_range("Age seems unrealistic!");
    }
    this->age = age;
}

3️⃣ Standard Exception Classes: Your Ready-Made Safety Nets

C++ comes with a family of exception types ready to use. They all live in <stdexcept>.

graph TD A["exception"] --> B["logic_error"] A --> C["runtime_error"] B --> D["invalid_argument"] B --> E["domain_error"] B --> F["length_error"] B --> G["out_of_range"] C --> H["range_error"] C --> I["overflow_error"] C --> J["underflow_error"]

The Most Useful Ones

Exception When to Use Example
runtime_error Something failed at runtime File not found
invalid_argument Bad input value Negative age
out_of_range Index too big/small Array index 100 in size-10 array
logic_error Bug in program logic Called function in wrong order
overflow_error Number too big Adding huge numbers

Using Standard Exceptions

#include <stdexcept>
#include <vector>

int getElement(vector<int>& v, int index) {
    if (index < 0 || index >= v.size()) {
        throw out_of_range("Index is out of bounds!");
    }
    return v[index];
}

The .what() Method

Every standard exception has a what() method that tells you what went wrong:

try {
    throw runtime_error("Disk is full!");
} catch (exception& e) {
    cout << e.what();  // Prints: Disk is full!
}

4️⃣ Custom Exceptions: Building Your Own Safety Nets

Sometimes the standard exceptions don’t fit your needs. You can create your own!

Simple Custom Exception

class NegativeBalanceError : public exception {
public:
    const char* what() const noexcept override {
        return "Balance cannot be negative!";
    }
};

Custom Exception with Details

class InsufficientFundsError : public runtime_error {
private:
    double requested;
    double available;

public:
    InsufficientFundsError(double req, double avail)
        : runtime_error("Insufficient funds"),
          requested(req), available(avail) {}

    double getRequested() const { return requested; }
    double getAvailable() const { return available; }
};

Using Your Custom Exception

class BankAccount {
    double balance = 100.0;

public:
    void withdraw(double amount) {
        if (amount > balance) {
            throw InsufficientFundsError(amount, balance);
        }
        balance -= amount;
    }
};

int main() {
    BankAccount account;
    try {
        account.withdraw(500.0);  // Too much!
    } catch (InsufficientFundsError& e) {
        cout << e.what() << endl;
        cout << "You wanted: quot; << e.getRequested();
        cout << ", You have: quot; << e.getAvailable();
    }
}

5️⃣ noexcept: Promising Not to Throw

Sometimes you want to promise that a function will never throw an exception. That’s what noexcept does.

Why Use noexcept?

  1. Performance: The compiler can optimize better
  2. Clarity: Other programmers know it’s safe
  3. Required: Move constructors often need it

Adding noexcept

// This function promises: "I will NEVER throw!"
int add(int a, int b) noexcept {
    return a + b;
}

// Conditional noexcept
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(T(move(a)))) {
    // noexcept if T's move constructor is noexcept
}

What Happens If You Break the Promise?

void dangerous() noexcept {
    throw runtime_error("Oops!");  // BAD!
}
// If this throws, std::terminate() is called
// Your program CRASHES immediately!

⚠️ Warning: If a noexcept function throws, your program terminates immediately! Only use it when you’re 100% sure.

Checking noexcept

cout << noexcept(add(1, 2));  // true
cout << noexcept(divide(1, 0));  // false

6️⃣ Stack Unwinding: How Exceptions Travel

When you throw an exception, C++ does something magical called stack unwinding. It’s like rewinding a video—going back through all the functions, cleaning up as it goes.

The Journey of an Exception

graph TD A["main calls functionA"] --> B["functionA calls functionB"] B --> C["functionB calls functionC"] C --> D["💥 functionC throws!"] D --> E["functionC destroyed/cleaned up"] E --> F["functionB destroyed/cleaned up"] F --> G["functionA destroyed/cleaned up"] G --> H["main catches exception"]

What Gets Cleaned Up?

During unwinding, destructors are called for all local objects:

class Logger {
public:
    Logger(string name) : name(name) {
        cout << "Creating " << name << endl;
    }
    ~Logger() {
        cout << "Destroying " << name << endl;
    }
private:
    string name;
};

void innerFunction() {
    Logger log3("Inner");
    throw runtime_error("Problem!");
}

void middleFunction() {
    Logger log2("Middle");
    innerFunction();
}

void outerFunction() {
    Logger log1("Outer");
    middleFunction();
}

int main() {
    try {
        outerFunction();
    } catch (exception& e) {
        cout << "Caught: " << e.what() << endl;
    }
}

Output:

Creating Outer
Creating Middle
Creating Inner
Destroying Inner
Destroying Middle
Destroying Outer
Caught: Problem!

💡 Key Insight: Every object created before the throw gets properly destroyed! This is why RAII (Resource Acquisition Is Initialization) works so well in C++.


7️⃣ Exception Safety Guarantees: Promises About Behavior

When you write functions, you can make guarantees about what happens if an exception occurs.

The Three Levels

graph LR A["No Guarantee 💀"] --> B["Basic Guarantee ✅"] B --> C["Strong Guarantee 💪"] C --> D["No-Throw Guarantee 🛡️"]

1. Basic Guarantee (Minimum Standard)

Promise: “If I throw, your data won’t be corrupted, and no resources will leak.”

void basicSafe(vector<int>& v, int value) {
    v.push_back(value);  // Might throw
    // If it throws, vector is still valid
    // (maybe unchanged, maybe modified)
}

2. Strong Guarantee (Transactional)

Promise: “If I throw, everything stays exactly as it was before you called me.”

void strongSafe(vector<int>& v, int value) {
    vector<int> temp = v;    // Copy first
    temp.push_back(value);   // Modify copy
    swap(v, temp);           // Swap only if successful
    // If push_back throws, original v unchanged!
}

3. No-Throw Guarantee (Bulletproof)

Promise: “I will NEVER throw an exception.”

void noThrowSafe(int& a, int& b) noexcept {
    int temp = a;
    a = b;
    b = temp;
    // Simple operations that can't fail
}

Real-World Example: Safe Assignment

class Document {
    string* data;

public:
    // Strong guarantee with copy-and-swap
    Document& operator=(Document other) {
        // other is a copy (passed by value)
        swap(data, other.data);  // noexcept
        return *this;
        // old data destroyed when other goes out of scope
    }
};

Which Guarantee Should You Choose?

Guarantee When to Use
Basic Minimum acceptable; always provide this
Strong When rolling back is important
No-throw Destructors, swap, move operations

🎬 Putting It All Together

Here’s a complete example showing everything working together:

#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;

// Custom exception
class WithdrawalError : public runtime_error {
    double amount;
public:
    WithdrawalError(double amt, const string& msg)
        : runtime_error(msg), amount(amt) {}
    double getAmount() const noexcept { return amount; }
};

class BankAccount {
    double balance;
    string owner;

public:
    BankAccount(string name, double initial)
        : owner(name), balance(initial) {}

    // Strong guarantee
    void withdraw(double amount) {
        if (amount < 0) {
            throw invalid_argument("Amount must be positive");
        }
        if (amount > balance) {
            throw WithdrawalError(amount, "Insufficient funds");
        }
        balance -= amount;  // Only happens if checks pass
    }

    // No-throw guarantee
    double getBalance() const noexcept {
        return balance;
    }
};

int main() {
    try {
        BankAccount account("Alice", 100.0);

        account.withdraw(30.0);
        cout << "Balance: quot; << account.getBalance() << endl;

        account.withdraw(200.0);  // This will throw!

    } catch (WithdrawalError& e) {
        cout << "Error: " << e.what() << endl;
        cout << "Tried to withdraw: quot; << e.getAmount() << endl;
    } catch (invalid_argument& e) {
        cout << "Invalid input: " << e.what() << endl;
    } catch (...) {
        cout << "Something unexpected happened!" << endl;
    }

    cout << "Program continues safely!" << endl;
    return 0;
}

Output:

Balance: $70
Error: Insufficient funds
Tried to withdraw: $200
Program continues safely!

🎯 Quick Summary

Concept One-Liner
try-catch Wrap risky code, handle problems gracefully
throw Signal that something went wrong
Standard Exceptions Ready-made exception types for common problems
Custom Exceptions Create your own for specific needs
noexcept Promise a function never throws
Stack Unwinding Automatic cleanup when exceptions travel
Safety Guarantees Promises about behavior during exceptions

🚀 You Did It!

You now understand how C++ exceptions work—from throwing problems to catching them safely. Like a skilled tightrope walker, you know how to set up safety nets, handle falls gracefully, and keep your program running smoothly even when things go wrong.

Remember: Exceptions are for exceptional situations. Use them wisely, and your code will be both robust and elegant!

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.