C++ Exception Handling Complete Guide
이 글의 핵심
Master C++ exceptions: standard hierarchy, catch-by-reference, exception safety guarantees, RAII patterns, noexcept, exceptions vs error codes, and production patterns.
Basic Exception Handling
#include <iostream>
#include <stdexcept>
using namespace std;
int divide(int a, int b) {
if (b == 0) {
throw runtime_error("Cannot divide by zero");
}
return a / b;
}
int main() {
try {
int result = divide(10, 0);
cout << result << endl;
} catch (runtime_error& e) {
cout << "Error: " << e.what() << endl;
}
return 0;
}
Output:
Error: Cannot divide by zero
Standard Exception Hierarchy
#include <exception>
#include <stdexcept>
// Base exception
exception
// Logic errors
logic_error
├─ invalid_argument
├─ domain_error
├─ length_error
└─ out_of_range
// Runtime errors
runtime_error
├─ range_error
├─ overflow_error
└─ underflow_error
Hierarchy visualization:
graph TD
A[std::exception] --> B[std::logic_error]
A --> C[std::runtime_error]
B --> D[std::invalid_argument]
B --> E[std::out_of_range]
B --> F[std::length_error]
C --> G[std::range_error]
C --> H[std::overflow_error]
style A fill:#FFB6C1
style B fill:#87CEEB
style C fill:#90EE90
Multiple Exception Handling
try {
// Code that may throw
} catch (out_of_range& e) {
cout << "Out of range: " << e.what() << endl;
} catch (invalid_argument& e) {
cout << "Invalid argument: " << e.what() << endl;
} catch (exception& e) {
cout << "Other error: " << e.what() << endl;
} catch (...) {
cout << "Unknown error" << endl;
}
Catch order matters: Catch specific types first, then base types. Otherwise, base type catches everything and specific handlers never execute.
Practical Examples
Example 1: File Processing Exception
#include <fstream>
#include <iostream>
#include <stdexcept>
using namespace std;
string readFile(const string& filename) {
ifstream file(filename);
if (!file.is_open()) {
throw runtime_error("Cannot open file: " + filename);
}
string content, line;
while (getline(file, line)) {
content += line + "\n";
}
return content;
}
int main() {
try {
string content = readFile("config.txt");
cout << content << endl;
} catch (runtime_error& e) {
cerr << "Error: " << e.what() << endl;
return 1;
}
return 0;
}
Explanation: Throwing exceptions for file errors makes error handling explicit and propagates failures clearly.
Example 2: Custom Exception Class
#include <iostream>
#include <exception>
#include <string>
using namespace std;
class InvalidAgeException : public exception {
private:
string message;
public:
InvalidAgeException(int age) {
message = "Invalid age: " + to_string(age);
}
const char* what() const noexcept override {
return message.c_str();
}
};
class Person {
private:
string name;
int age;
public:
Person(string n, int a) : name(n) {
if (a < 0 || a > 150) {
throw InvalidAgeException(a);
}
age = a;
}
void print() {
cout << name << ", " << age << " years old" << endl;
}
};
int main() {
try {
Person p1("Alice", 25);
p1.print();
Person p2("Bob", 200); // Throws exception
p2.print();
} catch (InvalidAgeException& e) {
cerr << "Error: " << e.what() << endl;
}
return 0;
}
Output:
Alice, 25 years old
Error: Invalid age: 200
Explanation: Domain-specific exceptions make errors more expressive and easier to handle.
Example 3: RAII and Exception Safety
#include <iostream>
#include <memory>
using namespace std;
class Resource {
public:
Resource() { cout << "Resource allocated" << endl; }
~Resource() { cout << "Resource released" << endl; }
void use() { cout << "Resource used" << endl; }
};
void dangerousFunction() {
throw runtime_error("Error occurred!");
}
int main() {
try {
// ❌ Manual management (leaks on exception)
// Resource* r = new Resource();
// dangerousFunction();
// delete r; // Never executed!
// ✅ RAII (automatic cleanup)
unique_ptr<Resource> r = make_unique<Resource>();
r->use();
dangerousFunction();
// Automatically released even on exception
} catch (exception& e) {
cout << "Error: " << e.what() << endl;
}
return 0;
}
Output:
Resource allocated
Resource used
Resource released
Error: Error occurred!
Explanation: Smart pointers ensure resources are safely released even when exceptions occur.
Common Issues
Issue 1: Catching by Value
Symptom: Exception object slicing Cause: Catching by value loses derived class information Solution:
// ❌ Wrong: catch by value
try {
throw runtime_error("error");
} catch (exception e) { // Catch by value
// Derived class information lost
}
// ✅ Correct: catch by reference
try {
throw runtime_error("error");
} catch (exception& e) { // Catch by reference
cout << e.what() << endl;
}
// ✅ const reference (safer)
catch (const exception& e) {
cout << e.what() << endl;
}
Key: Always catch by reference (const exception&) to avoid object slicing and preserve polymorphic behavior.
Issue 2: Throwing from Destructor
Symptom: Program abnormal termination Cause: Exception from destructor during stack unwinding calls terminate() Solution:
// ❌ Dangerous
class Resource {
public:
~Resource() {
throw runtime_error("error"); // Never do this!
}
};
// ✅ Correct
class Resource {
public:
~Resource() noexcept {
try {
// Risky operation
} catch (...) {
// Swallow exception
}
}
};
Issue 3: Exception Specification
Symptom: Warning or error when using throw() Cause: throw() is deprecated in C++11 Solution:
// ❌ Old style (deprecated)
void func() throw(int, runtime_error) {
// ...
}
// ✅ Use noexcept
void func() noexcept { // Doesn't throw
// ...
}
void func2() noexcept(false) { // May throw
// ...
}
try-catch Basics Summary
- try: Code block that may throw exceptions.
- throw: Thrown type is matched to first matching
catchby type. Catch derived classes before base classes to avoid slicing. - catch (…): Catches any type; usually placed last. Log before rethrowing to avoid silent swallow.
try {
mayThrow();
} catch (const std::invalid_argument& e) {
// Specific handling
} catch (const std::exception& e) {
// Common standard exception handling
} catch (...) {
// Unknown exceptions
}
Catch by reference: catch (const std::exception& e) is safer and more efficient than catching by value.
Exceptions vs Error Codes
| Aspect | Exceptions | Error Codes (expected, optional, bool) |
|---|---|---|
| Control flow | Clean for rare failures | Suitable for frequent, expected failures |
| Performance | Exception path is relatively expensive | Predictable in hot loops |
| C interop | Difficult to mix with C APIs | Works well with errno, return codes |
| Visibility | Not always obvious from signature (pre-C++17) | Explicit in return type |
| C++23 std::expected<T, E>: Models “value or error” explicitly, enabling failure propagation without exceptions. Team convention of “exceptions only for truly exceptional failures” reduces confusion. |
RAII and Exception Safety
RAII: Resources acquired in constructor, released in destructor. Even when exceptions occur, stack unwinding calls destructors, preventing leaks.
Exception Safety Guarantees
- Basic guarantee: No resource leaks; invariants may be temporarily broken but object remains valid.
- Strong guarantee: Commit-or-rollback (transaction-like; copy-and-swap idiom).
- Nothrow guarantee: Operation doesn’t throw exceptions (expressible with
noexcept). In production, knowing standard container exception guarantees (e.g., whatvector::push_backguarantees on failure) simplifies design. For custom classes, document what happens to class invariants when copy/move assignment throws.
noexcept Deep Dive
- Meaning: “This function doesn’t throw exceptions out.” Violation typically leads to
std::terminate. - Move operations:
std::vectoruses move during reallocation if move constructor isnoexcept. Adding noexcept to move constructor/assignment directly impacts performance. - Destructor: Destructors are implicitly
noexcept. Never throw from destructors.
void swap(MyType& a, MyType& b) noexcept {
// Only calls member swap, guarantees no exceptions
}
Conditional noexcept: noexcept(expr) form allows conditional specification.
Production Patterns
Pattern 1: Constructor Failure
Constructors have no return value, so for invalid state, throw exception (or use factory + expected).
class Database {
public:
Database(const string& connStr) {
if (!connect(connStr)) {
throw runtime_error("Connection failed");
}
}
};
Pattern 2: Catch Only at Boundaries
Library internals propagate exceptions; catch at UI/main for logging/recovery.
// Library code: propagate
void processData() {
if (error) throw DataException("...");
}
// Application boundary: catch
int main() {
try {
processData();
} catch (const exception& e) {
log(e.what());
return 1;
}
return 0;
}
Pattern 3: std::nested_exception
Wrap low-level exceptions to preserve cause when propagating upward.
#include <exception>
#include <stdexcept>
#include <iostream>
void lowLevel() {
throw std::runtime_error("Low-level error");
}
void midLevel() {
try {
lowLevel();
} catch (...) {
std::throw_with_nested(std::runtime_error("Mid-level error"));
}
}
int main() {
try {
midLevel();
} catch (const std::exception& e) {
std::cout << e.what() << std::endl;
try {
std::rethrow_if_nested(e);
} catch (const std::exception& nested) {
std::cout << " Caused by: " << nested.what() << std::endl;
}
}
}
Pattern 4: Testing Exceptions
Use test frameworks like Catch2 to verify exception types.
// Catch2 example
TEST_CASE("Division by zero throws") {
REQUIRE_THROWS_AS(divide(10, 0), std::runtime_error);
}
Pattern 5: Team Convention
Establish team convention: hot loops use error codes, I/O/parsing failures use exceptions.
Best Practices
1. Catch by const Reference
// ✅ Best
catch (const std::exception& e) {
// ...
}
// ❌ Avoid: catch by value (slicing)
catch (std::exception e) {
// ...
}
2. Order Specific to General
try {
// ...
} catch (const std::out_of_range& e) {
// Specific
} catch (const std::logic_error& e) {
// More general
} catch (const std::exception& e) {
// Most general
} catch (...) {
// Unknown
}
3. Don’t Swallow Silently
// ❌ Silent swallow
catch (...) {
// Nothing—hides errors!
}
// ✅ Log and rethrow
catch (...) {
log("Unknown exception");
throw; // Rethrow
}
4. Use RAII for Resources
// ✅ Smart pointers, RAII wrappers
auto file = std::make_unique<FileHandle>("data.txt");
// Automatically closed even on exception
5. noexcept for Move and Destructor
class MyClass {
public:
MyClass(MyClass&&) noexcept;
~MyClass() noexcept;
};
Exception Safety Guarantees
Basic Guarantee
No resource leaks; object remains in valid (but potentially unspecified) state.
void push_back(const T& value) {
// If exception during reallocation, no leaks
// but vector state may change
}
Strong Guarantee
Commit-or-rollback: operation succeeds completely or leaves state unchanged.
// Copy-and-swap idiom
T& operator=(const T& other) {
T temp(other); // Copy
swap(*this, temp); // Swap (noexcept)
return *this;
// If copy throws, *this unchanged
}
Nothrow Guarantee
Operation never throws exceptions.
void swap(T& a, T& b) noexcept {
// Guaranteed no exceptions
}
Exceptions vs Error Codes vs expected
Comparison Table
| Feature | Exceptions | Error Codes | std::expected (C++23) |
|---|---|---|---|
| Control flow | For exceptional failures | For expected failures | Explicit value-or-error |
| Performance | Slow on exception path | Predictable branching | Similar to error codes |
| Visibility | Not obvious in signature | Explicit in return | Explicit in return |
| Propagation | Automatic | Manual checking | Manual checking |
| C interop | Difficult | Easy | Easy |
When to Use Each
Exceptions:
- Rare, unexpected failures
- Constructor failures (no return value)
- Deep call stacks needing error propagation
- I/O, parsing, validation errors Error Codes:
- Frequent, expected failures (e.g., “not found”)
- Performance-critical hot loops
- C API interop
- When failure is part of normal flow std::expected (C++23):
- Explicit value-or-error modeling
- When you want error code benefits with type safety
- API boundaries where failure is common
// C++23 std::expected example
#include <expected>
std::expected<int, std::string> divide(int a, int b) {
if (b == 0) {
return std::unexpected("Division by zero");
}
return a / b;
}
int main() {
auto result = divide(10, 0);
if (result) {
std::cout << "Result: " << *result << "\n";
} else {
std::cout << "Error: " << result.error() << "\n";
}
}
RAII and Exception Safety
RAII (Resource Acquisition Is Initialization): Acquire resources in constructor, release in destructor. Stack unwinding during exception propagation calls destructors, preventing leaks.
RAII Example
#include <iostream>
#include <fstream>
class FileGuard {
std::FILE* file_;
public:
FileGuard(const char* path) : file_(std::fopen(path, "r")) {
if (!file_) throw std::runtime_error("Cannot open file");
}
~FileGuard() noexcept {
if (file_) std::fclose(file_);
}
std::FILE* get() { return file_; }
};
void processFile(const char* path) {
FileGuard file(path); // RAII: auto-closes on exception
// Use file.get()...
// Even if exception occurs, destructor closes file
}
Exception Safety in Practice
Knowing standard container guarantees helps design:
vector::push_back: Strong guarantee (if reallocation fails, vector unchanged)vector::reserve: Strong guaranteevector::insert: Basic guarantee (may partially modify) For custom classes, document exception guarantees in copy/move operations.
Production Patterns
Pattern 1: Boundary Exception Handler
#include <iostream>
#include <exception>
int main() {
try {
// Application logic
runApplication();
} catch (const std::exception& e) {
std::cerr << "Fatal error: " << e.what() << "\n";
logError(e);
return 1;
} catch (...) {
std::cerr << "Unknown fatal error\n";
return 2;
}
return 0;
}
Pattern 2: Exception Translation
// Translate low-level exceptions to domain exceptions
void apiCall() {
try {
lowLevelOperation();
} catch (const std::system_error& e) {
throw DatabaseException("Database connection failed", e);
}
}
Pattern 3: Scope Guard
#include <functional>
class ScopeGuard {
std::function<void()> cleanup_;
bool dismissed_ = false;
public:
ScopeGuard(std::function<void()> f) : cleanup_(std::move(f)) {}
~ScopeGuard() noexcept {
if (!dismissed_) {
try {
cleanup_();
} catch (...) {
// Log but don't rethrow
}
}
}
void dismiss() { dismissed_ = true; }
};
// Usage
void transactionalOperation() {
beginTransaction();
ScopeGuard guard([]() { rollback(); });
// Operations...
commit();
guard.dismiss(); // Success: don't rollback
}
FAQ
Q1: Are exceptions slow?
A: If no exception is thrown, overhead is minimal. Exceptions are slow only when thrown and caught. For normal flow, performance impact is negligible.
Q2: When to use exceptions?
A:
- Unrecoverable errors
- Constructor failures
- Deep call stack error propagation When NOT to use:
- Normal control flow
- Performance-critical loops
Q3: return vs throw?
A:
- return: Normal termination, expected result
- throw: Abnormal situation, error
Q4: Must I catch all exceptions?
A: Catch only exceptions you can handle. If you can’t handle, let it propagate upward. Catching everything and swallowing hides problems.
Q5: Why use noexcept?
A:
- Enables compiler optimizations
- Essential for move constructors (container optimization)
- Clarifies intent
Q6: Exceptions vs error codes?
A:
- Exceptions: Cleaner code, easier error propagation
- Error codes: Performance-critical, C compatibility needed
Summary
Key Points
- Catch by reference: Avoid object slicing
- Standard hierarchy:
exception→logic_error,runtime_error - RAII: Automatic resource cleanup on exception
- Exception safety: Basic, strong, nothrow guarantees
- noexcept: For move, destructor, swap
- Exceptions vs codes: Choose based on failure frequency and performance needs
Exception Handling Checklist
- Catching by const reference?
- Specific exceptions before general?
- RAII for all resources?
- Destructors don’t throw?
- Move operations marked noexcept?
- Logging before swallowing exceptions?
Related Articles
- C++ Exception Handling | try-catch-throw & Exceptions vs Error Codes
- [Go Error Handling: Forget try-catch](/en/blog/go-error-handling-guide/
- C++ Exception Specifications
Keywords
C++ exceptions, try-catch, throw, RAII, exception safety, noexcept, error handling One-line summary: C++ exceptions provide clean error propagation with RAII for automatic resource cleanup. Use for rare failures, error codes for frequent failures, and always catch by reference.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 예외 처리 | try-catch-throw와 예외 vs 에러 코드, 언제 뭘 쓸지
- C++ 스마트 포인터 | unique_ptr/shared_ptr ‘메모리 안전’ 가이드
- C++ Exception Specifications | ‘예외 명세’ 가이드
이 글에서 다루는 키워드 (관련 검색어)
C++, exceptions, try-catch, throw, RAII, exception safety 등으로 검색하시면 이 글이 도움이 됩니다.