Appearance
Exception Handling
Exceptions are for when the usual plan can't continue. Use them to report real failures (bad input, I/O errors, contract violations), not to steer ordinary control flow.
What problem do exceptions solve?
Without exceptions, every function must return error codes and every caller must remember to check them. That's noisy and fragile. Exceptions separate the happy path from the failure path and ensure cleanup happens automatically.
The core idea
cpp
int toInt(const std::string& s) {
size_t p = 0;
long v = std::stol(s, &p);
if (p != s.size()) throw std::invalid_argument("trailing characters");
if (v < INT_MIN || v > INT_MAX) throw std::out_of_range("int range");
return static_cast<int>(v);
}
try {
int x = toInt("123");
} catch (const std::invalid_argument& e) {
// bad format
} catch (const std::out_of_range& e) {
// number too big/small
} catch (const std::exception& e) {
// fallback handler
}- Throw with
throw Twhen you can't complete the function's contract. - Catch by
const&. Order from most specific to most general.
What really happens when you throw?
The runtime starts "stack unwinding": it walks back up the call stack and destroys every automatic (stack) object along the way.
cpp
struct File {
FILE* f {nullptr};
explicit File(const char* p) : f(std::fopen(p, "rb")) {
if (!f) throw std::runtime_error("open failed");
}
~File() { if (f) std::fclose(f); }
};
void readConfig(const char* path) {
File file(path); // will close on success or failure
std::vector<char> buf(4096); // also cleaned up on throw
// ... parse; may throw
} // everything is cleaned up in reverse orderMake sure your code manages resources properly so that they are released when an exception is thrown. RAII helps you in this, we'll discuss it in more detail later.
Designing your exceptions
- Derive from
std::exception(directly or viastd::runtime_error, etc.). - Use types to signal categories (e.g.,
ConfigError,NetworkError). - Include actionable context in the message (key, file, value).
cpp
struct ConfigError : std::runtime_error {
using std::runtime_error::runtime_error;
};
void loadCfg(const std::string& p) {
if (!std::filesystem::exists(p))
throw ConfigError("config not found: " + p);
}Add context when rethrowing across layers:
cpp
try {
loadCfg("/etc/app.cfg");
} catch (const std::exception& e) {
throw std::runtime_error(std::string("init failed: ") + e.what());
}To rethrow the same exception without losing type info, use throw; inside a catch block.
Catch-all with catch (...)
catch (...) matches anything (including exceptions not derived from std::exception). Use it sparingly:
- You don't get the exception object; prefer specific catches first.
- Good for last-resort logging, cleanup, or translating at module/thread boundaries.
Examples:
cpp
// 1) Last-resort log + rethrow (preserve original type)
try {
doWork();
} catch (...) {
auto ep = std::current_exception();
// log ep (rethrow to inspect if needed)
throw; // keep original exception
}
// 2) Translate unknown errors into a uniform type
try {
doWork();
} catch (...) {
throw std::runtime_error("worker failed");
}
// 3) Guard a noexcept boundary: never let exceptions escape
void tick() noexcept {
try { risky(); }
catch (...) { /* log; cannot throw here */ }
}noexcept: promise and consequences
noexcept means "this function will not throw". Benefits:
- The optimizer is freer (especially for move operations and containers).
- It documents a strong guarantee. If it throws anyway, the program calls
std::terminate().
Mark moves and tiny utilities noexcept when you're sure:
cpp
struct Buffer {
std::unique_ptr<int> data; size_t n {};
Buffer(Buffer&&) noexcept = default;
Buffer& operator=(Buffer&&) noexcept = default;
};Exception safety you can aim for
- Basic: no leaks, invariants hold; state may change.
- Strong: commit/rollback; if it throws, state is unchanged.
- Nothrow: never throws.
A simple strong-guarantee pattern:
cpp
void set_data(std::vector<int>& dst, std::vector<int> src) {
// src is a copy; if construction of src failed, we never got here.
std::swap(dst, src); // commit; old data lives in src and is destroyed
}Do's and don'ts
- Do reserve exceptions for exceptional situations; avoid them in hot inner loops.
- Do catch by
const&and place specific handlers before generic ones. - Do make RAII your default for resource ownership.
- Don't throw from destructors. If cleanup can fail, provide an explicit
close()that can report errors. - Don't use exceptions to implement normal control flow.
- Don't leave objects half-initialized---establish invariants early so unwinding can't observe a broken state.
A few patterns worth knowing
Add layers of context:
cpp
#include <exception>
template<class F>
void with_context(F&& f) {
try { f(); }
catch (const std::exception& e) {
throw std::runtime_error(std::string("context: ") + e.what());
}
}Boundary handoff (threads/tasks) where exceptions can't cross:
cpp
std::exception_ptr runAsync(std::function<void()> f) {
try { f(); return {}; }
catch (...) { return std::current_exception(); }
}
void joinAndReport(std::exception_ptr eptr) {
if (eptr) {
try { std::rethrow_exception(eptr); }
catch (const std::exception& e) { /* log e.what() */ }
}
}In short: throw when you must, build RAII types so cleanup is automatic, promise noexcept when you can, and design operations to provide at least the basic guarantee---prefer the strong guarantee for public interfaces.
Questions
Q: What happens during stack unwinding when an exception is thrown?
When an exception is thrown and propagates, C++ performs stack unwinding: it calls destructors for automatic (stack) objects in reverse construction order, ensuring RAII-managed resources are released.
Q: When should a function be marked noexcept?
Mark functions noexcept when they are guaranteed not to throw or when failure would be unrecoverable; this enables optimizations (e.g., move operations) and communicates strong guarantees to callers.
Q: Which catch form preserves the dynamic type information best?
Catching by reference (catch(Base&)) avoids slicing and preserves dynamic type, allowing polymorphic handling.
Q: What is the 'basic exception safety' guarantee?
Basic guarantee: if an operation throws, invariants hold and no leaks occur, but state may be modified. Strong guarantee means rollback semantics (no state change).
Q: How do you rethrow the current exception correctly?
A: throw e;
Use 'throw;' inside a catch block to rethrow the current exception without slicing or losing the original type.