Error Handling

std::expected

Although optional values are great for representing a value that is not necessarily available, there is an obvious disadvantage: they do not tell us the reason for the absence of a value. Whenever a database query or a file parsing goes wrong, the empty state returned means that the operation has failed but does not tell us anything about the reasons.

To address this issue, C++23 brings a new class template representing either the expected result of an operation or an error value telling us what has happened.

Expected-result objects may store either a successful value or an error value at the moment. Similar to optional objects, they usually store their active value directly without any dynamic allocations. An error value is stored inside a special helper object indicating that we have an unexpected result.

The following example shows a function parsing a port number from a string. It can fail for several reasons: the input is empty, non-numeric or out of range. These issues can be represented with a scoped enumeration. And the function will return either a parsed number or one of the error values.

#include <iostream>
#include <expected>
#include <string_view>
#include <string>

enum class ParseError {
    EmptyInput,
    InvalidCharacters,
    OutOfRange
};

std::expected<int, ParseError> parse_port(std::string_view input) {
    // Check this condition before entering the block
    if (input.empty()) {
        return std::unexpected(ParseError::EmptyInput);
    }

    for (char c : input) {
        if (c < '0' || c > '9') {
            return std::unexpected(ParseError::InvalidCharacters);
        }
    }

    int port = std::stoi(std::string(input));
    if (port < 1 || port > 65535) {
        return std::unexpected(ParseError::OutOfRange);
    }

    return port;
}

Expected-result objects store either a success value or an error value. In contrast to optional objects, they retain the information about the reason of the result being unavailable. They are useful for the expected failures allowing to inspect and handle them locally, without using exceptions.

In case when the input data is correct, the function will return an integer directly, and the compiler will construct a successful expected-result object. In case of failure, the function will return an unexpected result containing the error value.

Thus, this code separates successful results from the failures without the need for output parameters.

One can inspect the result by using the object either in a conditional expression or by calling a state-checking member function. In case of a value presence, one can access it using the dereference or member access operator. In case of an operation failure, there is a special member function allowing to access the stored error.

int main() {
    // Let the initializer determine this variable type
    auto result = parse_port("8080");

    if (result) {
        std::cout << "Successfully parsed port: " << *result << "\n";
    } else {
        std::cout << "Failed to parse port. Error code: " << static_cast<int>(result.error()) << "\n";
    }

    return 0;
}

Check the state of the object before accessing either its success value or its error value. Construct an error state using the unexpected-result helper.

Some more advanced implementations of the Standard Library may provide operations that chain transformations and propagate errors automatically. But sometimes it is simpler to use an explicit check. The error type should be cheap to return and expressive enough to help the caller to make a decision.

In case one needs to get the success value and throw an exception in case of operation failure, one can use checked access. In case when the object stores an error, checked access throws a dedicated exception which is parameterized by the error type.

Also, the exception stores the original error value, making it possible to inspect it using an accessor.

int main() {
    auto result = parse_port("invalid_port");

    try {
        int port = result.value();
        std::cout << "Port: " << port << "\n";
    // Bind by reference without allowing modification through it
    } catch (const std::bad_expected_access<ParseError>& e) {
        std::cout << "Error occurred: " << static_cast<int>(e.error()) << "\n";
    }

    return 0;
}

There is also a fallback member function that returns the success value in case when it is present and some default value in case of error storage.

Like optional objects, expected-result objects strictly follow the types and cannot contain reference types directly.

In order to represent a reference on the success path, one should use a pointer type as the success type or wrap the reference in a reference-wrapper utility inside the expected-result object.

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.