# Exceptions, noexcept and std::expected — C++

Source: https://www.skillbyai.com/en/cpp/a-errors

> Choose between exceptions and error values, and write exception-safe code.

## Reporting failure

C++ supports **exceptions**: `throw` an object (usually derived from `std::exception`, such as `std::invalid_argument` or `std::runtime_error`), and **`try`/`catch`** it higher up, catching by **const reference** (`catch (const std::exception& e)`). During **stack unwinding**, destructors of all automatic objects run, which is why RAII makes exception handling safe. Exception-safety guarantees describe functions: **no-throw** (marked **`noexcept`**, important for move constructors and destructors, which containers rely on), **strong** (an operation either succeeds or leaves state unchanged) and **basic** (no leaks, valid state). Exceptions suit rare, truly exceptional failures. For **expected failures** such as parsing user input, many codebases return values instead: `std::optional` when there is no detail, and **`std::expected<T, E>`** (C++23) to return either a value or an error. Some domains, such as games and embedded systems, compile with exceptions disabled and use error codes throughout. Whatever the style, be consistent within a codebase and never let errors pass silently.

## Stack unwinding

When an exception is thrown, frames are unwound and their destructors run until a matching handler is found.

![A stack of frames with a lightning bolt at the top frame and arrows peeling frames away downward until reaching a frame with a net icon.](assets/figures/cpp/section-7-map.svg) — Figure 7.1 — Exception propagation and destructor calls during unwinding.

## Exceptions for exceptional cases, expected for expected ones

Both styles side by side.

```cpp
#include <expected>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>

std::string read_config(const std::string& path) {
    std::ifstream in(path);
    if (!in) throw std::runtime_error("cannot open " + path);    // exceptional: config must exist
    return {std::istreambuf_iterator<char>(in), {}};
}

enum class ParseError { Empty, NotANumber, OutOfRange };

std::expected<int, ParseError> parse_age(const std::string& s) {   // expected failures
    if (s.empty()) return std::unexpected(ParseError::Empty);
    int v = 0;
    for (char c : s) {
        if (c < '0' || c > '9') return std::unexpected(ParseError::NotANumber);
        v = v * 10 + (c - '0');
        if (v > 150) return std::unexpected(ParseError::OutOfRange);
    }
    return v;
}

int main() {
    try {
        auto cfg = read_config("app.conf");
    } catch (const std::exception& e) {
        // log e.what() and exit with an error code
    }
    if (auto age = parse_age("34"); age) { /* *age == 34 */ }
    else if (age.error() == ParseError::NotANumber) { /* tell the user */ }
}
```

## Mark move operations noexcept

`std::vector` uses a type's move constructor during reallocation only if it is `noexcept`; otherwise it copies to preserve the strong guarantee. Forgetting `noexcept` on moves can silently make code much slower.

**Quiz:** How should exceptions usually be caught in C++?

- [x] By const reference
- [ ] By value
- [ ] By pointer
- [ ] With catch(...) only

*Answer:* By const reference. Catching by const reference avoids slicing and unnecessary copies.
