Error Handling in C++: From try/catch to Modern Approaches
\n\nThe exception mechanism in C++ is a powerful tool that requires deep understanding. While basic use of try/catch is known to most developers, professional error handling involves much more: from exception safety guarantees to alternative strategies without exceptions. In this article, we will explore advanced error handling techniques in modern C++.
1. Exception Safety Guarantees: The Foundation of Reliable Code
\n\nAny function that can throw an exception must provide one of three safety guarantees:
\n\n- \n
- Basic Guarantee: in case of an exception, object invariants are preserved, and there are no resource leaks. The object may be in an unpredictable but valid state. \n
- Strong Guarantee: the operation either completes fully, or the program state remains unchanged (atomicity). Achieved through the copy-and-swap idiom. \n
- Noexcept Guarantee: the function never throws exceptions. Critical for destructors, move operations, and operations used in containers. \n
// Example of strong guarantee via copy-and-swap\nclass Vector {\n int* data;\n size_t sz, cap;\npublic:\n void push_back(int value) {\n if (sz == cap) {\n // 1. Create a copy\n Vector tmp(*this);\n // 2. Modify the copy (may throw an exception)\n tmp.reserve(cap * 2);\n // 3. Swap (noexcept!)\n swap(tmp);\n }\n data[sz++] = value; // noexcept\n }\n \n void swap(Vector& other) noexcept {\n std::swap(data, other.data);\n std::swap(sz, other.sz);\n std::swap(cap, other.cap);\n }\n};\n\n2. RAII as the Foundation of Resource Management
\n\nThe RAII (Resource Acquisition Is Initialization) idiom is not just about smart pointers. It is a fundamental principle that helps avoid resource leaks during exceptions:
\n\n- \n
- Resource is acquired in the constructor \n
- Released in the destructor (which is
noexceptby default) \n - An exception in a destructor is UB (undefined behavior) \n
// RAII wrapper for a file descriptor\nclass FileDescriptor {\n int fd;\npublic:\n explicit FileDescriptor(const char* path) \n : fd(open(path, O_RDONLY)) {\n if (fd == -1) throw std::runtime_error("Failed to open file");\n }\n \n ~FileDescriptor() noexcept {\n if (fd != -1) close(fd);\n }\n \n // Prohibit copying, allow move\n FileDescriptor(FileDescriptor&& other) noexcept : fd(other.fd) {\n other.fd = -1;\n }\n};\n\n3. Modern Alternatives to Exceptions: std::optional and std::expected
\n\nNot all errors should be handled via exceptions. For expected errors (invalid input, missing data), it is better to use wrapper types:
\n\n#include <optional>\n#include <expected> // C++23\n\n// std::optional — for optional values\nstd::optional<int> safe_divide(int a, int b) {\n if (b == 0) return std::nullopt;\n return a / b;\n}\n\n// std::expected — for errors with information (C++23)\nstd::expected<double, std::string> sqrt_checked(double x) {\n if (x < 0) return std::unexpected("Negative input");\n return std::sqrt(x);\n}\n\n// Usage\nauto result = safe_divide(10, 0);\nif (result) {\n std::cout << *result;\n} else {\n std::cout << "Division by zero";\n}\n\n4. noexcept: Specification and Optimizations
\n\nThe noexcept specifier is not just documentation. It affects performance and