Menu

C++ Exceptions: throw, what(), and Error Handling

Exceptions report errors that a function cannot handle locally. Learn how to throw, what the standard exception types are, the what() message, and why exceptions beat return codes for the failures that matter.

This page includes runnable editors - edit, run, and see output instantly.

Why Exceptions Exist

On the previous page you used enum class to give error states meaningful names. That's great for outcomes a function expects and the caller is supposed to inspect. But some failures are different: a function deep in your call stack discovers that a file won't open, or that an argument makes no sense, and it has no idea what the program should do about it. Returning an error code only works if every caller in the chain remembers to check it and pass it up. Miss one check and the program sails on with garbage.

Exceptions solve this. When something goes wrong, you throw an object. Execution stops immediately, the stack unwinds (every local object between the throw and the handler has its destructor run), and control jumps to the nearest matching catch. An unhandled exception can't be silently ignored - if nothing catches it, the program calls std::terminate and aborts.

This page focuses on the throwing side - the error objects themselves. The next page digs into the try/catch machinery in detail.

Throwing and the what() Message

You can technically throw any value - throw 42; or throw "oops"; are legal - but don't. The convention everyone follows is to throw an object derived from std::exception. That base class declares one virtual method, what(), returning a const char* description of the problem. Sticking to the convention means a single catch (const std::exception& e) can handle anything.

The <stdexcept> header gives you ready-made types whose constructor takes the message:

Notice what() returns the exact string you constructed the exception with. Notice too that we caught by const exception& even though we threw a runtime_error - that works because runtime_error is a std::exception (a relationship you'll recognize from the inheritance page).

The Standard Exception Hierarchy

Before writing your own exception type, check whether the standard library already has one that fits. They all inherit from std::exception and split into two families in <stdexcept>:

  • logic_error - a bug in the program's logic that could, in principle, be caught before running. Subtypes include invalid_argument, out_of_range, domain_error, and length_error.
  • runtime_error - a failure that only shows up at runtime and isn't a programming mistake per se. Subtypes include range_error, overflow_error, and underflow_error.

Many library functions throw these for you. For example, the vector container's at() does bounds checking and throws out_of_range instead of letting you read past the end:

That at() is the safe counterpart of v[9]. The plain operator[] does no bounds check - reading v[9] here is undefined behavior, not an exception. Choosing at() is how you turn a silent corruption into a catchable error.

Pick the type that describes the error: invalid_argument when a caller passes something nonsensical, out_of_range for index/key problems, runtime_error for "the outside world failed on me."

Writing Your Own Exception Type

When no standard type fits - you want to attach extra data, or catch your error specifically and nothing else - define a class that inherits from std::exception (or one of its subtypes) and override what(). Inheriting from std::runtime_error is the easiest route because it already stores the message and implements what() for you:

Because NetworkError carries a status code, the handler can react to it - retry on a 5xx, give up on a 4xx. A bare error string couldn't do that. The custom type also lets a catch (const NetworkError&) grab only network problems and leave everything else to the more general handler below it.

If you ever inherit straight from std::exception (not runtime_error), remember to override what() yourself, and mark it noexcept to match the base signature:

class ParseError : public std::exception {
public:
    const char* what() const noexcept override {
        return "failed to parse input";
    }
};

Throw by Value, Catch by Reference

This is the single most important rule of C++ exceptions, and the one beginners get wrong. Throw objects by value and catch them by const reference.

throw runtime_error("oops");            // by value - correct
catch (const runtime_error& e) { ... }  // by const reference - correct

Catching by value instead - catch (std::exception e) - copies the exception into a base-class object and slices off the derived part. After slicing, e.what() calls the base implementation, not your overridden one, so your carefully crafted message disappears:

try {
    throw NetworkError(503, "service unavailable");
} catch (std::exception e) {       // by value - object slicing!
    std::cout << e.what();         // generic message, status() is gone
}

The reference (&) preserves the real dynamic type, so virtual what() dispatches correctly and you can still access derived members. Add const because you're only reading the exception, not modifying it. Never throw a pointer (throw new runtime_error(...)) - the catcher would have to delete it, and on which code path? That's exactly the leak exceptions are supposed to prevent.

Next: try-catch

You can now create and throw well-formed exceptions and choose the right standard type for each failure. The other half of the story is the catching side. The next page covers try/catch in full: ordering multiple catch blocks from most-specific to most-general, the catch-all catch (...), re-throwing with a bare throw;, and how RAII (think back to smart pointers) guarantees your resources are released as the stack unwinds.

Frequently Asked Questions

What is an exception in C++?

An exception is an object that signals an error which the current function can't handle on its own. You throw it, the stack unwinds (destroying local objects along the way), and a matching catch block higher up takes over. It separates the code that detects a problem from the code that decides what to do about it.

What is the difference between throw and return for errors?

A return value has to be checked by the caller, and it's easy to forget - the program just carries on with bad data. A thrown exception cannot be ignored: if nobody catches it, the program terminates. Exceptions are for genuine failures (a file won't open, input is invalid); return values are still right for ordinary results, including expected "not found" cases.

What does the what() method do in C++ exceptions?

Every class derived from std::exception provides a virtual what() method that returns a const char* describing the error. When you catch an exception, calling e.what() gives you the human-readable message you can log or print. The standard exception types set it from the string you pass to their constructor.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED