Error Handling Patterns
Modern approaches to error handling in C++
Exceptions vs Expected
Exceptions
- • Use for exceptional circumstances
- • Stack unwinding is expensive
- • Non-local control flow
- • Hard to track all possible throws
std::expected (C++23)
- • Use for expected failures
- • Zero overhead
- • Explicit in type signature
- • Monadic error handling
Expected Pattern
#include <expected>
enum class ParseError {
InvalidFormat,
OutOfRange
};
std::expected<int, ParseError> parse_int(const std::string& s) {
try {
size_t pos;
int value = std::stoi(s, &pos);
if (pos != s.size()) {
return std::unexpected(ParseError::InvalidFormat);
}
return value;
} catch (const std::out_of_range&) {
return std::unexpected(ParseError::OutOfRange);
} catch (...) {
return std::unexpected(ParseError::InvalidFormat);
}
}
// Usage
auto result = parse_int("42");
if (result) {
std::cout << "Value: " << *result << std::endl;
} else {
switch (result.error()) {
case ParseError::InvalidFormat:
std::cerr << "Invalid format" << std::endl;
break;
case ParseError::OutOfRange:
std::cerr << "Number too large" << std::endl;
break;
}
}Monadic Error Handling
// Chain operations with error handling
auto result = parse_int("100")
.and_then([](int n) -> std::expected<int, ParseError> {
if (n < 0) return std::unexpected(ParseError::OutOfRange);
return n * 2;
})
.transform([](int n) {
return std::to_string(n);
})
.or_else([](ParseError) {
return std::expected<std::string, ParseError>{"default"};
});
// No explicit error checking in the chain!
// Errors short-circuit automaticallyRAII for Cleanup
// Use RAII for guaranteed cleanup
class FileHandle {
FILE* file_;
public:
explicit FileHandle(const char* path, const char* mode)
: file_(fopen(path, mode)) {
if (!file_) throw std::runtime_error("Failed to open file");
}
~FileHandle() { if (file_) fclose(file_); }
// Disable copy
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Enable move
FileHandle(FileHandle&& other) : file_(other.file_) {
other.file_ = nullptr;
}
FILE* get() const { return file_; }
};
// Usage - cleanup guaranteed even if exception thrown
void process_file() {
FileHandle file("data.txt", "r");
// Use file.get()...
// File automatically closed when function exits
}Best Practices
- ✓Use exceptions for truly exceptional errors (programming bugs, resource exhaustion)
- ✓Use std::expected for expected domain failures (parsing, validation)
- ✓Use std::optional for nullable values
- ✓Never throw from destructors
- ✗Don't use exceptions for control flow
- ✗Don't throw from noexcept functions