Menu

C++ try-catch: Handle Exceptions the Right Way

Wrap risky code in try, react in catch. Learn how to catch exceptions by const reference, order multiple handlers, use catch (...), and rethrow - without leaking resources.

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

From Throwing to Handling

On the previous page you learned how to throw an exception when something goes wrong. Throwing is only half the story - an exception that is never caught calls std::terminate and crashes your program. The try/catch statement is how you handle what was thrown and keep running.

The shape is simple: put the risky code inside a try block, and follow it with one or more catch blocks that react to specific error types. If the try block runs cleanly, every catch is skipped. The moment something throws, control jumps straight to the first matching catch.

Notice that "after" never prints. As soon as the throw fires, the rest of the try block is abandoned and execution resumes inside the matching catch. After the catch finishes, the program continues normally below it.

Catch by const Reference

The single most important habit in C++ error handling: catch exceptions by const reference, not by value.

Catching by value copies the exception, and worse, it slices it. The standard exceptions form a hierarchy (runtime_error and logic_error both derive from std::exception), so catching a derived exception as a base value chops off the derived part. Catching by reference keeps the object intact and polymorphic:

Here we throw an out_of_range but catch it as a const exception&. Because out_of_range derives from exception, the base-class handler matches, and the reference means e.what() still returns the real message. Had you written catch (exception e) (by value), the object would be sliced to a plain exception and you could lose the specific message.

Multiple catch Blocks

A single try can be followed by several catch blocks, each for a different exception type. C++ tries them top to bottom and runs the first one that matches - so order them from most specific to most general.

Because invalid_argument is more specific than exception, it must come first. If you flipped the order and put catch (const exception&) on top, it would swallow every exception - the invalid_argument handler below it would become dead code that can never run. Many compilers warn about this, but the language won't stop you.

catch (...) and Rethrowing

Sometimes you want a safety net for anything you didn't anticipate. The catch-all handler catch (...) matches every exception type, including ones that don't derive from std::exception (someone can throw 42; or throw "oops";).

The catch is that you get no object - there's no e to inspect. So catch (...) is best used as a last resort: log that something failed, or clean up and rethrow.

To rethrow the current exception - pass it up to an outer handler after doing some local cleanup or logging - use a bare throw; with no operand. This preserves the original exception (its real type and message), unlike throw e;, which would re-throw a sliced copy:

The inner handler logs and rethrows; the outer handler in main then deals with it. Use bare throw; for this, never throw e;.

Stack Unwinding and RAII

When an exception propagates out of a try block, C++ performs stack unwinding: every local object between the throw and the matching catch has its destructor called, in reverse order of construction. This is what makes exceptions safe - resources held by stack objects get released automatically.

This is exactly why you should hold resources in RAII types (like std::vector, std::string, and smart pointers) rather than raw new/delete. Watch what happens when an exception cuts across a manual allocation:

void leaky() {
    int* buffer = new int[1000];
    mightThrow();        // if this throws, the next line never runs...
    delete[] buffer;     // ...and the buffer leaks
}

Because the throw jumps over delete[], the memory is lost. A smart pointer fixes it for free - its destructor runs during unwinding:

void safe() {
    auto buffer = std::make_unique<int[]>(1000);
    mightThrow();   // if this throws, buffer's destructor still frees the memory
}                   // no manual delete, no leak, even on the exception path

The takeaway: don't try to catch an exception just to delete something. Let destructors do the cleanup, and reserve catch for decisions about how to recover.

Common Mistakes and Gotchas

A handful of traps come up again and again:

Don't use exceptions for normal control flow. Throwing and unwinding is far slower than a simple if. Reserve exceptions for genuinely exceptional, error conditions - not for "the user typed an empty string."

An empty catch block hides bugs. Writing catch (...) {} to silence an error means failures vanish without a trace. At minimum log the problem; usually you should rethrow or handle it properly.

A destructor that throws is dangerous. If a destructor throws during stack unwinding (while another exception is already in flight), the program calls std::terminate. Destructors are implicitly noexcept in modern C++ - never let an exception escape one.

catch only sees what try covers. An exception thrown before entering the try, or in a different function that isn't on the call path inside it, won't be caught here. The catch only protects code that runs inside its own try block (directly or in functions it calls).

Next: Undefined Behavior

Exceptions are the defined way C++ tells you something went wrong - you throw, you catch, the behavior is predictable. But C++ also has a darker corner where the language makes no promises at all: dereferencing a dangling pointer, reading past the end of an array, signed integer overflow. The next page covers undefined behavior - what triggers it, why it can appear to "work" right up until it catastrophically doesn't, and how to keep it out of your code.

Frequently Asked Questions

How does try-catch work in C++?

You put code that might throw inside a try { } block. If an exception is thrown, the program stops running the rest of the try block and jumps to the first matching catch block, where you handle the error. If nothing throws, the catch blocks are skipped entirely.

Why should you catch exceptions by const reference in C++?

Catching by reference (catch (const std::exception& e)) avoids copying the exception object and, crucially, preserves polymorphism - so a derived exception caught as its base type still calls the right what(). Catching by value (catch (std::exception e)) slices off the derived part and can lose information.

How do you catch any exception in C++?

Use catch (...) - the ellipsis catches every exception regardless of type. It's a useful last-resort handler, but since you get no object to inspect, put it after your specific catch blocks and use it mainly to log or rethrow.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED