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 thecatch
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 bothinvalid_argument
andruntime_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
withunique_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 fromstd::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 wheregetline
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 asystem_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.