Exception Handling in C++: try, catch, throw, noexcept and RAII

Exception Handling in C++ gives you a clean, structured way to detect errors, report them, and recover without crashing or scattering checks everywhere. In this guide, you’ll master try, catch, and throw with clear examples, outputs, best practices, and “Try it yourself” challenges after each section.

🔹 What is Exception Handling in C++?

Exception Handling in C++ separates normal logic from error-handling logic. When something goes wrong, you throw an exception. Control jumps to the nearest matching catch handler inside a try block, allowing you to respond or recover gracefully.

  • try: Wraps code that might fail.
  • throw: Signals an error condition with a value (an exception).
  • catch: Handles specific exception types.
// Minimal example showing try, throw, catch
#include <iostream>
using namespace std;
int divInt(int a, int b) {
    if (b == 0) {
        // throw a standard exception
        throw runtime_error("division by zero"); 
    }
    return a / b;
}
int main() {
    try {
        cout << divInt(10, 0) 
        << "\n"; // risky call
    } catch (const runtime_error& e) {
        cout << "Caught error: " 
        << e.what() << "\n";
    }
    cout << "Program continues...\n";
}

Output

Caught error: division by zero
Program continues...

Try it yourself

  • Change the divisor to a non-zero value and confirm the catch block is skipped.
  • Throw a different exception type (e.g., logic_error) and adapt the catch type.

🔹 Syntax: try, catch, throw (The Essentials)

Use throw where the error occurs, and handle it where you can decide recovery. Catch exceptions by const reference to avoid slicing and unnecessary copies. Order catch blocks from most-specific to most-generic.

// Multiple catches and a catch-all handler
#include <iostream>
#include <stdexcept>
using namespace std;
void risky(int code) {
    if (code == 1) 
        throw invalid_argument("bad input");
    if (code == 2) 
        throw runtime_error("runtime issue");
    if (code == 3) 
        throw 42; // throwing a non-standard type (int)
}
int main() {
    for (int c : {1, 2, 3}) {
        try {
            risky(c);
            cout << "No exception for code=" 
            << c << "\n";
        } catch (const invalid_argument& e) {
            cout << "invalid_argument: " 
            << e.what() << "\n";
        } catch (const runtime_error& e) {
            cout << "runtime_error: " 
            << e.what() << "\n";
        } catch (...) { // catch-all (last resort)
            cout << "Unknown exception caught\n";
        }
    }
}

Output

invalid_argument: bad input
runtime_error: runtime issue
Unknown exception caught

Best practice: prefer throwing and catching standard exceptions (derived from std::exception) so you can use what() for messages and integrate with libraries naturally.

Try it yourself

  • Add a catch (const exception& e) before the catch-all and observe it catching both invalid_argument and runtime_error.
  • Reorder handlers incorrectly (generic before specific) and see how the compiler warns or how it changes behavior.

🔹 Stack Unwinding and RAII (Automatic Cleanup)

When an exception is thrown, C++ unwinds the stack: local objects are destroyed in reverse order. This is why RAII (Resource Acquisition Is Initialization) is crucial—resources are released automatically even when errors occur.

// Demonstrate automatic cleanup (stack unwinding)
#include <iostream>
#include <stdexcept>
using namespace std;
struct Guard {
    const char* name;
    explicit Guard(const char* n) : name(n) {
        cout << "Acquire " 
        << name << "\n";
    }
    ~Guard() {
        cout << "Release " 
        << name << "\n";
    }
};
void f() {
    Guard g1("g1");
    {
        Guard g2("g2");
        throw runtime_error("boom"); // unwinding starts here
    }
}
int main() {
    try {
        f();
    } catch (const exception& e) {
        cout << "Caught: " 
        << e.what() << "\n";
    }
    cout << "Continue...\n";
}

Output

Acquire g1
Acquire g2
Release g2
Release g1
Caught: boom
Continue...

Use RAII types (e.g., smart pointers, file wrappers) to ensure cleanup, instead of manual new/delete or open/close scattered across try/catch paths.

Try it yourself

  • Create a small RAII File wrapper that opens in the constructor and closes in the destructor. Throw inside the scope and confirm it closes.
  • Replace raw new with unique_ptr and confirm no leaks on exceptions.

🔹 Custom Exceptions (Derive from std::exception)

Defining your own exception types makes errors more specific and catchable. Derive from std::exception or one of its subclasses and override what() for diagnostic messages.

// Custom exception with helpful message
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
class ConfigError : public runtime_error {
public:
    explicit ConfigError(const string& msg) : 
             runtime_error("ConfigError: " + msg) {}
};
void loadConfig(const string& path) {
    if (path.empty()) {
        throw ConfigError("path is empty");
    }
    // Imagine I/O here...
}
int main() {
    try {
        loadConfig("");
    } catch (const ConfigError& e) {
        cout << e.what() << "\n";
    } catch (const exception& e) {
        cout << "Other error: " 
        << e.what() << "\n";
    }
}

Output

ConfigError: path is empty

Common standard exceptions you can reuse: std::invalid_argument, std::out_of_range, std::runtime_error, std::logic_error, std::bad_alloc, std::system_error.

Try it yourself

  • Create ParseError derived from std::runtime_error that includes line/column in the message.
  • Throw ParseError from a simple CSV parser when a field is missing.

🔹 Rethrowing and Translating Exceptions

Sometimes you need to log or add context, then rethrow the same exception, or translate it into a domain-specific one. Use bare throw; to rethrow without losing the original exception type.

// Rethrow with extra context vs translate to a custom type
#include <iostream>
#include <stdexcept>
using namespace std;
void lowLevel() {
    throw runtime_error("disk read failed");
}
void midLevel() {
    try {
        lowLevel();
    } catch (const exception& e) {
        cerr << "midLevel note: " 
        << e.what() << "\n";
        throw; // rethrow same exception
    }
}
void highLevel() {
    try {
        midLevel();
    } catch (const exception& e) {
        throw runtime_error(string("highLevel context: ")
        + e.what()); // translate
    }
}
int main() {
    try {
        highLevel();
    } catch (const exception& e) {
        cout << "Final: " 
        << e.what() << "\n";
    }
}

Output

midLevel note: disk read failed
Final: highLevel context: disk read failed

Use rethrow for preserving original types and stack info; use translation when you want callers to rely on higher-level, domain-specific errors.

Try it yourself

  • Add a logger that records the exception message before rethrowing in midLevel().
  • Translate different low-level exceptions into a single AppError category.

🔹 noexcept, Functions That Must Not Throw

Use noexcept to declare a function that won’t throw exceptions. If it does, std::terminate is called. This helps optimizers and is important for move operations and destructors.

// noexcept on moves/destructors lets containers optimize
#include <iostream>
#include <utility>
using namespace std;
struct Buffer {
    int* p{nullptr};
    size_t n{0};
    Buffer() = default;
    explicit Buffer(size_t count) : p(new int[count]{}), n(count) {}
    ~Buffer() noexcept { delete[] p; }
    Buffer(Buffer&& other) noexcept : p(other.p), n(other.n) {
        other.p = nullptr; other.n = 0;
    }
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] p;
            p = other.p; n = other.n;
            other.p = nullptr; other.n = 0;
        }
        return *this;
    }
};

Guidelines: destructors should be noexcept; move operations ideally noexcept to enable container optimizations; avoid throwing from noexcept functions.

Try it yourself

  • Mark a function noexcept, then deliberately throw inside to observe termination behavior (do this only in a test program).
  • Mark move operations noexcept and benchmark vector reallocation vs non-noexcept moves.

🔹 Exceptions vs Error Codes

Exceptions separate error paths from normal flow and provide automatic cleanup. Error codes are fine for hot paths or low-level APIs where exceptions are disabled or too costly, but they can clutter logic if overused.

  • Use exceptions for recoverable, exceptional conditions in application code.
  • Use error codes or std::expected-like patterns for performance-critical or low-level boundaries.
  • Don’t mix styles arbitrarily; be consistent per module or layer.

Try it yourself

  • Refactor a function returning bool + out parameter into one that throws with context on failure; compare readability.
  • Write a thin adapter that catches exceptions and returns error codes for a C API boundary.

🔹 Mini Project: Safe File Reader with RAII

Combine Exception Handling in C++, RAII, and standard exceptions to build a safe file reader that reports errors clearly and never leaks resources.

#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
class FileReader {
    ifstream in;
public:
    explicit FileReader(const string& path) {
        in.open(path);
        if (!in) 
        throw runtime_error("cannot open file: " + path);
    }
    string readAll() {
        string data, line;
        while (getline(in, line)) {
            data += line;
            data += '\n';
        }
        if (!in.eof()) 
        throw runtime_error("read error");
        return data;
    }
};
int main() {
    try {
        FileReader fr("input.txt"); // ensure this file exists or expect exception
        string content = fr.readAll();
        cout << "File has " 
        << content.size() << " chars\n";
    } catch (const exception& e) {
        cerr << "I/O failure: " 
        << e.what() << "\n";
    }
    cout << "Done\n";
}

Possible Output

I/O failure: cannot open file: input.txt
Done

Note how resource management is automatic: ifstream closes on destruction, even on exceptions. The error message is clear and actionable.

Try it yourself

  • Add line counting and throw runtime_error with the line number where getline fails mid-read.
  • Add a noexcept logging destructor in another helper to confirm it never throws during unwinding.

🔹 Best Practices for Exception Handling in C++

  • Throw and catch by type; prefer types derived from std::exception.
  • Catch by const&; avoid catching by value to prevent slicing.
  • Keep exceptions for exceptional cases; don’t use them for routine control flow.
  • Write exception-safe code with RAII and strong invariants.
  • Don’t throw from destructors; if unavoidable, catch inside and log—never let it escape.
  • Use noexcept appropriately, especially on destructors and moves.
  • Document which functions may throw and what they throw.

Try it yourself

  • Audit one module: ensure all throws are standard or custom exceptions with clear messages.
  • Add RAII wrappers around any manual resource management you find.

🔹 FAQs about Exception Handling in C++

Q1: Should I use exceptions everywhere?
Use them where they improve clarity and safety. Avoid in ultra-low-latency hot paths or cross-language ABI boundaries; consider error codes or status objects there.

Q2: What types should I throw?
Prefer standard exceptions or your own types derived from std::exception. Avoid throwing raw integers or strings directly.

Q3: Can constructors throw?
Yes. If a constructor can’t establish a valid state, it should throw. RAII ensures already-constructed members are cleaned up safely.

Q4: What about exceptions in destructors?
Avoid. Destructors should not throw. If an error occurs, catch and log internally. Uncaught exceptions during stack unwinding call std::terminate.

Q5: Are exceptions slow?
The presence of try/catch usually has minimal overhead; the slow part is throwing. Use them for exceptional flows, not as normal control flow.

Q6: What’s the difference between catch (...) and catch(const exception&)?
catch (...) catches anything (including non-std::exception types) but gives no info. catch(const exception&) catches standard exceptions and lets you read what().

Q7: How do I propagate an exception after logging?
Use a bare throw; inside a catch block to rethrow the current exception without altering its type.

Q8: Should I mark functions noexcept?
Mark functions that must not throw (destructors, move ops) and where it enables optimizations. Don’t overuse it; throwing from noexcept terminates the program.

Q9: How do I handle exceptions at thread boundaries?
Catch inside threads and communicate errors via shared state, promises/futures, or rethrow in the joining thread with stored exceptions.

Q10: Can I mix exceptions with errno or OS error codes?
Yes. Wrap low-level errors into exceptions with context (e.g., include std::error_code in your message or custom type) for higher-level callers.

Try it yourself

  • Create a worker thread that throws; catch inside, store the message, and report it to the main thread via a promise/future.
  • Wrap errno from a failed POSIX call into a system_error and throw it.

🔹 Wrapping Up

Exception Handling in C++ lets you write safer, cleaner code by separating normal logic from error handling and ensuring automatic cleanup through RAII. Use try, catch, and throw thoughtfully, prefer standard or well-designed custom exceptions, and leverage noexcept and RAII for robust, maintainable systems. Practice the “Try it yourself” tasks to make these skills second nature.

Leave a Comment

About RadiantRiva

Your go-to resource for coding tutorials, developer guides, and programming tips.

Learn More

Quick Links

Follow Us

Newsletter

Get coding tips, tutorials, and updates straight to your inbox.