Error Handling Best Practices
Master comprehensive strategies for writing robust, error-resistant C++ applications
Learn professional-grade error handling strategies that make your C++ applications robust, reliable, and production-ready.
A Simple Example
#include <iostream>
#include <fstream>
#include <optional>
#include <string>
#include <stdexcept>
// Use std::optional for operations that might fail expectedly
std::optional<std::string> readFirstLine(const std::string& filename) {
std::ifstream file{filename};
if (!file) {
return std::nullopt; // Expected failure - file might not exist
}
std::string line;
if (std::getline(file, line)) {
return line;
}
return std::nullopt;
}
// Use exceptions for unexpected failures
class ConfigFile {
std::string data;
public:
explicit ConfigFile(const std::string& filename) {
std::ifstream file{filename};
if (!file) {
throw std::runtime_error{"Critical: Cannot open config file: " + filename};
}
std::string line;
while (std::getline(file, line)) {
data += line + "\n";
}
if (data.empty()) {
throw std::runtime_error{"Config file is empty"};
}
}
const std::string& getData() const { return data; }
};
int main() {
// Using optional for expected failures
auto line{readFirstLine("optional_file.txt")};
if (line) {
std::cout << "First line: " << *line << "\n";
} else {
std::cout << "File not found or empty - using defaults\n";
}
// Using exceptions for critical failures
try {
ConfigFile config{"app.config"};
std::cout << "Config loaded successfully\n";
} catch (const std::exception& e) {
std::cerr << "Fatal error: " << e.what() << "\n";
std::cerr << "Application cannot start without config\n";
return 1;
}
return 0;
}
Breaking It Down
Use std::optional for expected failures
- What it is: A container that may or may not hold a value
- When to use: Operations that commonly fail as normal behavior (lookup, search, parse)
- How to check: Use if (result) or result.has_value()
- Remember: Optional makes failure explicit in the return type
Use exceptions for unexpected failures
- What they are: Exceptional conditions that disrupt normal flow
- When to use: Unexpected errors that caller cannot prevent (config missing, memory exhausted)
- Catch specifically: Always catch by const reference to avoid slicing
- Remember: Exceptions should be exceptional, not for control flow
Exception safety guarantees
- Basic guarantee: No resource leaks, objects remain valid
- Strong guarantee: Operation succeeds completely or has no effect (atomic)
- No-throw guarantee: Operation never throws (mark with noexcept)
- Remember: Design functions with these guarantees in mind
RAII for resource management
- What it does: Ties resource lifetime to object lifetime
- Benefit: Automatic cleanup even when exceptions occur
- Example: std::unique_ptr, std::lock_guard, std::fstream
- Remember: RAII provides the basic exception safety guarantee
Why This Matters
- Professional code does not crash - it handles errors gracefully and maintains data integrity.
- The difference between hobby projects and production software is robust error handling.
- Users do not care what went wrong; they care that the application did not lose their data and gave them a clear message about what to do next.
- Good error handling is what makes software reliable and trustworthy.
Critical Insight
Exception safety has three levels: 1) Basic guarantee - no resource leaks, 2) Strong guarantee - operation succeeds completely or has no effect (atomic), 3) No-throw guarantee - never throws exceptions.
Design your error handling strategy around these guarantees. Use RAII (smart pointers, lock guards) for basic safety, validate inputs before modifying state for strong safety, and mark functions noexcept when you can guarantee no-throw. This creates layers of defense against errors.
Best Practices
Use std::optional for expected failures: When operations commonly fail as normal behavior (like lookups), use std::optional instead of exceptions.
Catch by const reference: Always catch exceptions as const std::exception& to avoid slicing and unnecessary copies.
Provide context in error messages: Include what went wrong, why it matters, and what to do about it. "Error" is useless.
Use RAII for resources: Smart pointers, lock guards, and RAII types ensure cleanup even when exceptions occur.
Mark no-throw functions noexcept: If a function guarantees not to throw, mark it noexcept to enable compiler optimizations.
Common Mistakes
Catching everything silently: catch(...) {} hides bugs and makes debugging impossible. Always log or handle errors.
Not providing context in error messages: "Error" is useless. Include what failed, the inputs, and what to do next.
Mixing error handling strategies: Choose exceptions OR error codes for a subsystem, do not mix both randomly.
Throwing from destructors: This can terminate your program during stack unwinding. Always catch in destructors.
Resource leaks with raw pointers: Using raw new/delete is error-prone. Always use smart pointers or containers.
Debug Challenge
This code has a resource leak when an exception occurs. Click the highlighted line to see the fix:
Quick Quiz
- When should you use std::optional instead of exceptions?
- What are the three levels of exception safety?
- Should you catch exceptions in destructors?
Practice Playground
Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.
Output:
Error:
Lesson Progress
- Fix This Code
- Quick Quiz
- Practice Playground - run once