Error Handling - Exceptions and Alternatives¶
C++ supports exceptions, error codes, std::optional, and std::expected (C++23). Choose the right mechanism for the context.
Key Facts¶
- Exceptions: for exceptional/unexpected errors. Zero-cost when not thrown (table-based unwinding)
noexceptspecifier: function promises not to throw. Enables optimizations (move in vector)- Error codes: for expected failures (file not found, invalid input). No overhead, explicit control flow
std::optional<T>(C++17): value-or-nothing. For functions that may not produce a resultstd::expected<T,E>(C++23): value-or-error. Type-safe alternative to exceptions- Standard exception hierarchy:
std::exception->runtime_error,logic_error, etc. - Catch by
constreference:catch (const std::exception& e) - Stack unwinding on exception: all local RAII objects destructed (see raii resource management)
throw;re-throws current exception (preserving type).throw e;slices tostd::exception- Never throw from destructor (terminates during stack unwinding)
- Exception specifications removed in C++17 except
noexcept
Patterns¶
Exception Handling¶
#include <stdexcept>
// Throwing
void parse(const std::string& input) {
if (input.empty())
throw std::invalid_argument("Input cannot be empty");
if (input.size() > 1024)
throw std::length_error("Input too long");
// parse...
}
// Catching
try {
parse(user_input);
} catch (const std::invalid_argument& e) {
std::cerr << "Invalid: " << e.what() << '\n';
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << '\n';
} catch (...) {
std::cerr << "Unknown error\n";
throw; // re-throw
}
Custom Exceptions¶
class AppError : public std::runtime_error {
int code_;
public:
AppError(int code, const std::string& msg)
: std::runtime_error(msg), code_(code) {}
int code() const { return code_; }
};
class NetworkError : public AppError {
public:
NetworkError(const std::string& msg)
: AppError(1001, msg) {}
};
// Usage
throw NetworkError("Connection refused");
std::optional (C++17)¶
#include <optional>
std::optional<int> find_index(const std::vector<int>& v, int target) {
for (size_t i = 0; i < v.size(); ++i) {
if (v[i] == target) return static_cast<int>(i);
}
return std::nullopt;
}
// Usage
if (auto idx = find_index(vec, 42)) {
std::cout << "Found at index " << *idx << '\n';
} else {
std::cout << "Not found\n";
}
// Value-or-default
int idx = find_index(vec, 42).value_or(-1);
std::expected (C++23)¶
#include <expected>
enum class ParseError { empty_input, invalid_format, overflow };
std::expected<int, ParseError> parse_int(std::string_view sv) {
if (sv.empty())
return std::unexpected(ParseError::empty_input);
// parse logic...
return 42;
}
// Usage
auto result = parse_int("123");
if (result) {
use(*result);
} else {
handle_error(result.error());
}
// Monadic operations (C++23)
auto final_result = parse_int(input)
.transform([](int x) { return x * 2; })
.or_else([](ParseError e) -> std::expected<int, ParseError> {
log_error(e);
return 0; // default value
});
noexcept¶
// Mark functions that won't throw
void swap(Widget& a, Widget& b) noexcept {
std::swap(a.data_, b.data_);
}
// Conditional noexcept
template<typename T>
void safe_swap(T& a, T& b) noexcept(noexcept(std::swap(a, b))) {
std::swap(a, b);
}
// Move operations MUST be noexcept for vector optimization
Widget(Widget&& other) noexcept;
Widget& operator=(Widget&& other) noexcept;
Gotchas¶
- Issue: Catching exception by value slices derived type -> Fix: Always catch by
constreference:catch (const std::exception& e) - Issue:
throw e;inside catch re-throws sliced copy -> Fix: Usethrow;to re-throw preserving original type - Issue: Exception in destructor during stack unwinding ->
std::terminate-> Fix: Never throw from destructors. Catch and log inside destructor. - Issue:
optional.value()throwsbad_optional_accessif empty -> Fix: Check withhas_value()/if (opt)or usevalue_or(default) - Issue: Missing
noexcepton move constructor makes vector copy instead of move during reallocation -> Fix: Always mark move opsnoexcept