Language[lang]
C++ try catch: how to throw and catch exceptions
- Section
- Language [lang]
- Standard
- C++17
- Compilers
- MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2, Clang 23.1 (libc++)
- Published
- Length
- 3 min read
In this answer10 clauses
- 1try, catch and throw: the syntax
- 2Throw by value, catch by const reference
- 2.1Rethrowing: throw; or throw e;
- 3The order of catch handlers
- 4What to throw: std::exception and your own type
- 5When nothing catches: std::terminate
- 5.1A handler in main
- 5.2Throwing from a noexcept function
- 6No finally: destructors do the cleanup
- 7Catching in a constructor: the function-try-block
- 8What exceptions cost, and when not to use them
- 9What each standard changed
- 10Checklist for exception handling in C++
Abstract
Put the code that can fail in a try block followed by catch handlers, and throw an object of a class derived from std::exception, such as std::runtime_error. Throw by value, catch by const reference, order the handlers from the most derived type to catch (...), and rethrow with throw;. Destructors release resources as the exception passes on its way to a handler, so most functions need no try block. An exception that nothing catches calls std::terminate.

Put the code that can fail in a try block, follow it with catch handlers, and report a failure with throw and an object of a class derived from std::exception, such as std::runtime_error. Throw by value, catch by const reference. Handlers are tried top to bottom: the most derived type first, catch (...) last; in a handler, throw; rethrows. C++ has no finally: on the way to a handler, destructors run as the exception leaves each scope, so most functions need no try block. An exception that nothing catches calls std::terminate.
This answer is written for C++17, which removed dynamic exception specifications and made noexcept part of a function’s type; the table at the end has each standard’s changes, through C++26. The samples were built by MSVC 19.44 and Clang 19.1 on Microsoft’s STL (Windows x64), GCC 16.2 with libstdc++ and Clang 23.1 with libc++ (Compiler Explorer, Linux x86-64): MSVC with /EHsc /W4, the others with -Wall -Wextra. The first releases with noexcept are in Since When’s C++11 table.
What changed: a function that must not throw is declared throw()noexcept.
since C++11
try, catch and throw: the syntax
The running example parses sensor readings such as t1=21.5 with std::stod (C++ string to int covers what each conversion does with bad input):
#include <iostream>
#include <stdexcept>
#include <string>
double parse_reading(const std::string& text) {
auto eq = text.find('=');
if (eq == std::string::npos)
throw std::runtime_error("no '=' in \"" + text + "\"");
return std::stod(text.substr(eq + 1));
}
int main() {
try {
std::cout << parse_reading("t1=21.5") << '\n'; // 21.5
std::cout << parse_reading("t2 21.7") << '\n'; // throws
std::cout << "not printed\n";
} catch (const std::exception& e) {
std::cout << "error: " << e.what() << '\n';
}
std::cout << "after the handler\n";
}— end example]
All four printed:
21.5
error: no '=' in "t2 21.7"
after the handler— end example]
Here the throw leaves parse_reading at once, the rest of the try block in main is skipped, and the first handler whose type matches runs; execution then continues after the last handler, not at the throw. e.what() returned the message the std::runtime_error was built with. A handler for a class also catches exceptions of every class derived from it publicly and unambiguously, which is why one const std:: handler is enough here.
A handler sees only what a throw throws, yours or a library’s: integer division by zero is undefined behavior ([expr.mul]), not an exception, and no handler is guaranteed to see it.
Throw by value, catch by const reference
Catch by const reference: a handler that takes std::exception by value copies only the base part of the thrown object, so the derived type is gone: that is slicing. The running example’s own exception type, ParseError, is a std::runtime_error with a line number; here it is caught both ways, and kind names the type the handler sees:
class ParseError : public std::runtime_error {
public:
ParseError(const std::string& what, int line)
: std::runtime_error(what), line_(line) {}
int line() const noexcept { return line_; }
private:
int line_;
};
void load() {
throw ParseError("no '=' in \"t3 21.9\"", 3);
}
const char* kind(const std::exception& e) {
return dynamic_cast<const ParseError*>(&e) ? "ParseError"
: "std::exception";
}
// ...
try {
load();
} catch (std::exception e) { // by value: a copy of the base part
std::cout << "by value: " << kind(e) << ", " << e.what()
<< '\n';
}
try {
load();
} catch (const std::exception& e) { // the object that was thrown
std::cout << "by reference: " << kind(e) << ", " << e.what()
<< '\n';
}— end example]
With libstdc++ (GCC) and libc++ (Clang 23.1) it printed:
by value: std::exception, std::exception
by reference: ParseError, no '=' in "t3 21.9"— end example]
Microsoft’s STL keeps the message in the base object, so with MSVC and Clang 19.1 the first line read std::: the text survived, the type did not. Only GCC warned, under -Wall: catching polymorphic type 'class std::. MSVC and both Clangs said nothing, even at /Wall and -Weverything.
Throw by value too. The Core Guidelines’ E.15 gives the reason for both: “Throwing by value (not by pointer) and catching by reference prevents copying, especially slicing base subobjects.”
Rethrowing: throw; or throw e;
Inside a handler, throw; rethrows the object being handled. throw e; throws a new exception copied from e, with the type the handler declared:
void load_rethrow() {
try {
load();
} catch (const std::exception&) {
std::cout << "load failed, passing it on\n";
throw; // the same ParseError object
}
}
void load_throw_e() {
try {
load();
} catch (const std::exception& e) {
std::cout << "load failed, passing it on\n";
throw e; // a new std::exception, copied from e
}
}— end example]
An outer handler for ParseError, then one for std::exception, received this on libstdc++ and libc++:
load failed, passing it on
got ParseError, line 3: no '=' in "t3 21.9"
load failed, passing it on
got std::exception: std::exception— end example]
After throw e; the ParseError handler no longer matched on any of the four; on Microsoft’s STL the last line kept the message.
The order of catch handlers
Handlers are tried in the order they appear and the first match wins ([except.handle]), so a handler for a base class placed first hides the handlers for its derived classes below it. Put the most derived type first and catch (...) last (Core Guidelines E.31):
try {
parse_reading("t3 21.9");
} catch (const std::exception& e) {
std::cout << "std::exception handler: " << e.what() << '\n';
} catch (const std::runtime_error& e) { // never runs
std::cout << "std::runtime_error handler: " << e.what() << '\n';
}— end example]
All four ran the first handler and warned about the second, even with no warning flag (MSVC at /W1); catch (...) before another handler is an error:
| Compiler | std::exception before std::runtime_error |
catch (...) before another handler |
|---|---|---|
| MSVC | warning C4286: 'const std:: |
error C2311: 'const std:: |
| Clang 19.1 and 23.1 | warning exception of type 'const std:: |
error catch-all handler must come last |
| GCC | warning exception of type 'std:: |
error '...' handler must be the last handler for its try block [-fpermissive] |
What to throw: std::exception and your own type
Throw a type made for the purpose, not a built-in type such as int (E.14). Derive it from std::runtime_error or std::logic_error, which store the message: one const std:: handler then catches it, and what() exists. Add the data a handler needs, as ParseError adds the line number; here load calls parse_reading for the lines t1=21.5, t2=21.7 and t3 21.9, counting them:
// copying the exception object cannot throw (E.16)
static_assert(std::is_nothrow_copy_constructible_v<ParseError>);
double parse_reading(const std::string& text, int line) {
auto eq = text.find('=');
if (eq == std::string::npos)
throw ParseError("no '=' in \"" + text + "\"", line);
return std::stod(text.substr(eq + 1));
}
// ...
try {
load(lines);
} catch (const ParseError& e) { // the type and its data
std::cout << "line " << e.line() << ": " << e.what() << '\n';
}
try {
load(lines);
} catch (const std::exception& e) { // any standard exception
std::cout << "error: " << e.what() << '\n';
}— end example]
All four printed line 3: no '=' in "t3 21.9", then error: no '=' in "t3 21.9", and the static_assert held on all three libraries.
std::exception itself takes no message in the standard. Passing one to its constructor built only on Microsoft’s STL. And throw 42; compiles, but a const std:: handler does not match an int: on all four that program ended in std::terminate.
The main standard exceptions and their bases:

When nothing catches: std::terminate
With no matching handler the program calls std::terminate, and whether the stack is unwound first is implementation-defined ([except.handle]). Nothing catches what load throws here:
struct Trace { // says when it is destroyed
const char* name;
~Trace() { std::cout << "~Trace " << name << std::endl; }
};
double parse_reading(const std::string& text) {
Trace t{"parse_reading"};
auto eq = text.find('=');
if (eq == std::string::npos)
throw std::runtime_error("no '=' in \"" + text + "\"");
return std::stod(text.substr(eq + 1));
}
void load() {
Trace t{"load"};
parse_reading("t3 21.9");
}
int main() {
Trace t{"main"};
std::cout << "loading" << std::endl;
load(); // nothing catches what it throws
std::cout << "loaded" << std::endl;
}— end example]
Every build printed loading, then ended, without optimization and at -O2 (/O2 on MSVC) alike:
| Build | Local destructors | stderr | Exit |
|---|---|---|---|
| MSVC and Clang 19.1 (Microsoft’s STL) | none ran | nothing | 0xC0000409 |
| GCC (libstdc++) | none ran | terminate called after throwing an instance of 'std::, then what(): no '=' in "t3 21.9" |
134, SIGABRT |
| Clang 23.1 (libc++) | none ran | libc++abi: terminating due to uncaught exception of type std:: |
134, SIGABRT |
With stdout on a pipe, a line still in std::cout’s buffer was lost on all four; these samples flush with std::endl.
A handler in main
Give main a handler for const std:: and a last catch (...) for anything else, each reporting the error and returning:
int main() {
try {
std::cout << parse_reading("t1=21.5") << '\n';
std::cout << parse_reading("t3 21.9") << '\n';
} catch (const std::exception& e) {
std::cerr << "error: " << e.what() << '\n';
return 1;
} catch (...) { // anything else; always last
std::cerr << "error: unknown exception\n";
return 1;
}
}— end example]
This parse_reading throws an int, and the second handler caught it: all four printed 21.5, wrote error: unknown exception to stderr and exited with 1. With a handler found, the functions between the throw and main unwind. A handler that passes the exception on does not save the program:
int main() {
Trace t{"main"};
std::cout << "loading" << std::endl;
try {
load();
} catch (...) {
throw; // passes it on, and nothing is left to catch it
}
std::cout << "loaded" << std::endl;
}— end example]
All four printed these lines, then ended as in the table:
loading
~Trace parse_reading
~Trace load— end example]
main’s own Trace, declared before the try, was never destroyed: the rethrow from main found no handler. A thread needs its own handler: an exception that leaves a thread’s function calls std::terminate ([except.terminate]), and std::thread and std::jthread shows how to hand it to the joining thread with std::exception_ptr.
Throwing from a noexcept function
An exception that would leave a noexcept function calls std::terminate too, even when a caller has a matching handler:
double parse_reading(const std::string& text) noexcept {
Trace t{"parse_reading"};
auto eq = text.find('=');
if (eq == std::string::npos)
throw std::runtime_error("no '=' in \"" + text + "\"");
return std::stod(text.substr(eq + 1));
}
// ...
try {
load();
} catch (const std::exception& e) { // matches, but never runs
std::cout << "caught: " << e.what() << std::endl;
}— end example]
No destructor ran on any build, and each ended as in the table above. All four warned, with no warning flag:
| Compiler | Warning |
|---|---|
| MSVC | C4297: 'parse_reading': function assumed not to throw an exception but does |
| Clang 19.1 and 23.1 | 'parse_reading' has a non-throwing exception specification but can still throw [-Wexceptions] |
| GCC | 'throw' will always call 'terminate' [-Wterminate] |
With the throw one call below the noexcept function, no compiler warned; clang-tidy’s bugprone-exception-escape still flagged the noexcept function. The standard leaves it implementation-defined whether the stack is “unwound, unwound partially, or not unwound at all” ([except.terminate]); here whether the lower function’s destructor ran differed by compiler, and on MSVC by optimization level: do not rely on it. Use noexcept where a throw is impossible or must end the program (E.12): when to mark a move constructor noexcept applies the rule to moves, and can a C++ destructor throw to destructors.
No finally: destructors do the cleanup
C++ has no finally block. As an exception leaves each function, the destructors of its local objects run in reverse order of construction: that is stack unwinding. Three frames, with a Trace in each:
double parse_reading(const std::string& text) {
Trace t{"parse_reading"};
auto eq = text.find('=');
if (eq == std::string::npos)
throw std::runtime_error("no '=' in \"" + text + "\"");
return std::stod(text.substr(eq + 1));
}
void load() {
Trace file{"load: file"};
Trace buffer{"load: buffer"};
parse_reading("t3 21.9");
std::cout << "not printed\n";
}
int main() {
Trace t{"main"};
try {
load();
} catch (const std::exception& e) {
std::cout << "main caught: " << e.what() << '\n';
}
std::cout << "main goes on\n";
}— end example]
All four printed:
~Trace parse_reading
~Trace load: buffer
~Trace load: file
main caught: no '=' in "t3 21.9"
main goes on
~Trace main— end example]

That is why a std::unique_ptr, a std::vector, a lock guard or a file stream needs no cleanup code on the error path, and why few functions need a try block (E.18). A function that cannot handle the error lets it pass (E.17), and its locals clean up (E.6); catch where you can act on it. A class whose members are such types needs no destructor of its own: that is the rule of zero.
Catching in a constructor: the function-try-block
To catch what a member’s initializer throws, write the constructor as a function-try-block. Its handler can log or translate the error but cannot swallow it: reaching the end of the handler rethrows.
class Sensor {
public:
explicit Sensor(const std::string& calibration)
try : offset_(parse_reading(calibration)) {
std::cout << "Sensor ready\n";
} catch (const std::exception& e) {
std::cout << "Sensor: " << e.what() << '\n';
} // the end of this handler rethrows
double offset() const { return offset_; }
private:
double offset_;
};
int main() {
try {
Sensor s("offset 0.5");
std::cout << "offset " << s.offset() << '\n'; // not printed
} catch (const std::exception& e) {
std::cout << "main: " << e.what() << '\n';
}
}— end example]
All four printed:
Sensor: no '=' in "offset 0.5"
main: no '=' in "offset 0.5"— end example]
A return in that handler is an error on all four (Clang: return in the catch of a function try block of a constructor is illegal), and using the object’s non-static members there is undefined behavior.
What exceptions cost, and when not to use them
WG21’s TR 18015 describes a table-based implementation in which a try block “incurs no run-time cost” while nothing throws, and objects with destructors cost nothing either. The tables enlarge the program image, and the time a throw takes to reach its handler “may be large, variable, and dependent on program size and complexity”. Microsoft documents its x64 exception handling as table-based, and the Itanium C++ ABI specifies a two-phase unwind: search, then clean up. None of the three gives a cost per throw.
So use exceptions for failures that are rare and handled far from where they occur. For failures that are part of normal operation (a malformed line in a messy file), return std::expected (C++23) or a value the caller checks.
Code built with -fno-exceptions cannot use try or throw: in a program like the first sample, Clang 23.1 rejected each throw and each try (cannot use 'throw' with exceptions disabled), and GCC reported exception handling disabled, use '-fexceptions' to enable once. A library function still throws there: std::stod("n/a") ended in std::terminate on both. For such code the Core Guidelines’ E.25 to E.27 say: simulate RAII, fail fast, or use error codes.
On MSVC, build with /EHsc. Built with /EHs-c- instead, MSVC warned C4530, and main’s handler still caught the exception, but none of the destructors in load or parse_reading ran (the /EH option).
What each standard changed
| Standard | Change | Compilers and libraries |
|---|---|---|
| C++11 | noexcept, std::exception_ptr, std:: |
noexcept since GCC 4.6, Clang 3.0 and Visual Studio 2015 |
| C++17 | dynamic exception specifications such as throw(std:: removed |
GCC and both Clangs reject them; MSVC 19.44 accepts them with warning C5040 |
| C++17 | noexcept is part of the function type |
all four (below) |
| C++17 | std::; throw(), deprecated since C++11, now means noexcept(true) |
all three libraries have std:: |
| C++20 | throw() removed |
all four still accept it as noexcept through C++26; only Clang warns, with -pedantic -Wdeprecated |
| C++23 | std::, with std::expected |
derives from std::exception on all three libraries |
| C++26 | a throw in constant evaluation, caught before the evaluation ends |
GCC 16.2 only |
| C++26 | std::, which returns std:: |
GCC 16.2 has an earlier form that returns const E*; none of the four has C++26’s |
Since C++17, a function declared noexcept and one that is not have different types:
double parse_reading(const std::string& text);
double parse_checked(const std::string& text) noexcept;
int main() {
std::cout << "same type: "
<< std::is_same<decltype(parse_reading),
decltype(parse_checked)>::value
<< '\n'; // 1 before C++17, 0 since
}— end example]
All four printed 1 as C++14 and 0 as C++17. As C++17, all four also rejected initializing a pointer to a noexcept function from parse_reading.
In C++26 a constant evaluation can throw, provided the exception is caught before the evaluation ends:
struct BadReading {
int line;
};
constexpr int parse_digit(char c, int line) {
if (c < '0' || c > '9')
throw BadReading{line};
return c - '0';
}
constexpr int digit_or(char c, int fallback) {
try {
return parse_digit(c, 1);
} catch (const BadReading&) {
return fallback;
}
}
static_assert(digit_or('7', -1) == 7); // no throw: C++20 and later
static_assert(digit_or('x', -1) == -1); // C++26: thrown and caught— end example]
Only GCC 16.2 built it, and only as C++26. The try alone, with nothing thrown, is C++20: the first static_assert built as C++20 on all four.
Checklist for exception handling in C++
- Throw by value an object of a class derived from
std::exception; derive your own fromstd::runtime_error. - Catch by
constreference. GCC’s-Wcatch-value(part of-Wall) and clang-tidy’smisc-throw-by-value-catch-by-referencefind the handlers that do not. - Order handlers from the most derived type to the base, with
catch (...)last. - Rethrow with
throw;, notthrow e;. - Keep resources in objects with destructors and write few try blocks: catch where you can act on the error.
- Give
maina handler forconst std::and a lastexception& catch (...), each reporting the error and returning, not rethrowing. - Mark a function
noexceptonly where a throw is impossible or must end the program;bugprone-exception-escapefinds a throw that can leave one. - Do not leave a handler empty;
bugprone-empty-catchflags one, even one holding only a comment without@TODOor@FIXME. - Build with
/EHscon MSVC.
More answers about the core language are in Language.
Questions
Should I catch exceptions by value or by reference in C++?
By const reference: catch (const std::exception& e). A handler that takes std::exception by value copies only the base part of the thrown object, so its derived type is lost, and with libstdc++ and libc++ its what() then returns only the text std::exception. The C++ Core Guidelines say the same in E.15: throw by value, catch exceptions from a hierarchy by reference.
What is the difference between throw; and throw e; in C++?
throw; rethrows the exception object being handled, with its type intact. throw e; throws a new exception copied from e, with the type the handler declared: in a handler for const std::exception&, a ParseError became a plain std::exception on MSVC 19.44, Clang 19.1, GCC 16.2 and Clang 23.1, and an outer handler for ParseError no longer caught it. Inside a handler, pass the exception on with throw;.
What happens if an exception is not caught in C++?
The program calls std::terminate, and whether the stack is unwound first is implementation-defined. Built with MSVC 19.44, Clang 19.1, GCC 16.2 and Clang 23.1, no local destructor ran. libstdc++ and libc++ printed the exception type and its what() to stderr and ended with SIGABRT; Microsoft’s runtime printed nothing and exited with 0xC0000409.
Does C++ have a finally block?
No. Cleanup goes in destructors: when an exception leaves a scope on its way to a handler, the destructors of its local objects run, so a std::unique_ptr, a std::vector or a lock guard releases what it holds on that path too. Where no such type fits, the C++ Core Guidelines (E.19) suggest a final_action object to express the cleanup.
Are C++ exceptions slow?
TR 18015 describes a table-based implementation in which a try block incurs no run-time cost while nothing throws; the price is larger tables in the program image, and a throw may take a large and variable time to reach its handler. TR 18015 gives no cost per throw. Use exceptions for failures that are rare and handled far from where they occur, and std::expected (C++23) or a return value for routine ones.
Should I derive from std::exception or std::runtime_error?
From std::runtime_error, or std::logic_error: both take a message in their constructor and return it from what(). The standard gives std::exception no constructor that takes a message; one that passed a string to it compiled only on Microsoft’s library, and libstdc++ and libc++ rejected it. Add the data a handler needs, such as a line number, as members of your class.
Sources
- [except.handle] Handling an exceptionC++ standard working draft
- [except.terminate] The std::terminate functionC++ standard working draft
- [std.exceptions] Exception classesC++ standard working draft
- throw expressioncppreference.com
- try blockcppreference.com
- noexcept specifiercppreference.com
- E: Error handlingC++ Core Guidelines
- ISO/IEC TR 18015: Technical Report on C++ PerformanceWG21