Recoverable Errors Today
English

Recoverable Errors Today

Section 9.1 covered failures that current scpp treats as unrecoverable: the program aborts as soon as a checked operation detects a bad state.

Not every problem belongs in that category. Sometimes a function should simply say “I could not produce a value,” and let the caller choose what to do next. Today, the main standard tool for that style is std::optional<T>.

An optional either contains one T value or contains nothing. That makes it a good fit for lookups, parsing steps, and other operations where “missing” is a normal outcome rather than a fatal bug.

For each runnable example below, save the file as optional.scpp, then build and run it like this:

scpp optional.scpp -o optional
./optional

For examples that are supposed to abort, the program compiles successfully, but terminates when it asks an empty optional for a value it does not have.

Return std::optional<T> when a function may or may not produce a value

A function can return an optional and let the caller branch on has_value().

import std;

std::optional<int> find_score(int id) {
    if (id == 7) {
        std::optional<int> full{99};
        return full;
    }
    std::optional<int> empty{};
    return empty;
}

int main() {
    std::optional<int> found = find_score(7);
    std::optional<int> missing = find_score(1);

    if (found.has_value()) {
        std::println("score = {}", found.value());
    }
    if (!missing.has_value()) {
        std::println("not found");
    }
    return 0;
}

Output:

score = 99
not found

The important shift is who makes the decision. find_score does not abort when it has no answer; it returns an empty optional, and the caller decides how to handle that ordinary case.

An empty optional does not need a default-constructible T

std::optional<T> can still be empty even when T itself requires constructor arguments.

import std;

class Ticket {
private:
    int id_{};

public:
    virtual ~Ticket() = default;

    Ticket(int id) : id_{id} {
        return;
    }

    Ticket(const Ticket& other) : id_{other.id_} {
        return;
    }

    int id() const {
        return this->id_;
    }
};

int main() {
    std::optional<Ticket> empty{};
    std::optional<Ticket> full{Ticket{42}};
    std::println("{} {}", empty.has_value(), full->id());
    return 0;
}

Output:

false 42

Ticket itself has no zero-argument constructor, but the optional can still represent “no ticket yet” by containing nothing.

reset() clears the current value, and another optional can replace it

Once an optional has a value, you can clear it back to the empty state and later replace it from another optional.

import std;

int main() {
    std::optional<int> answer{7};
    std::println("{} {}", answer.has_value(), answer.value());

    answer.reset();
    std::println("{}", answer.has_value());

    std::optional<int> replacement{9};
    answer = replacement;
    std::println("{}", answer.value());
    return 0;
}

Output:

true 7
false
9

That gives you a straightforward state machine: full, empty, then full again. The key is that the caller stays explicit about each transition.

value() on an empty optional is still a checked failure

std::optional only makes absence recoverable if code actually checks before accessing the value. Calling value() on an empty optional aborts.

import std;

int main() {
    std::optional<int> empty{};
    return empty.value();
}

Behavior when run: the program aborts because empty does not contain an int.

So std::optional<T> is not “nullable data with no rules.” The recoverable part is the explicit branch on has_value(), not unchecked access.

The recoverable-error pattern available today

So far, the practical rules are:

That last limitation is the natural bridge to the next section: preparing APIs for result types that can carry both a value and an error explanation.


← Previous: Unrecoverable Errors and Compiler-Inserted Checks · Table of Contents · Next: Recoverable Errors with std::expected<T, E>