Search code examples
c++exceptionerror-handlingc++23std-expected

When to use std::expected instead of exceptions


When should I use std::expected and when should I use exceptions? Take this function for example:

int parse_int(std::string_view str) {
    if (str.empty()) {
        throw std::invalid_argument("string must not be empty");
    }
    /* ... */
    if (/* result too large */) {
        throw std::out_of_range("value exceeds maximum for int");
    }
    return result;
}

I want to distinguish between different errors when using this function, so it's useful that I can throw different types of exceptions. However, I could also do that with std::expected:

enum class parse_error {
    empty_string,
    invalid_format,
    out_of_range
};

std::expected<int, parse_error> parse_int(std::string_view str) noexcept {
    if (str.empty()) {
        return std::unexpected(parse_error::empty_string);
    }
    /* ... */
    if (/* result too large */) {
        return std::unexpected(parse_error::out_of_range);
    }
    return result;
}

Are there any reasons to use std::expected over exceptions (performance, code size, compile speed, ABI), or is it just stylistic preference?


Solution

  • First of all, whatever error-handling strategy you are planning to use - establish it at the very beginning of the given project - see E.1: Develop an error-handling strategy early in a design. Because the idea of changing this strategy "later" will most probably result in having 2 strategies: the old one and the new one.

    Sometimes, the choice is easy: when, for whatever reasons, exceptions are not allowed in the given project, just use std::expected.

    It is really hard (I'd say, impossible) to propose one error handling strategy, that fits all needs. I can only put here just one recommendation, that I try to follow:


    The one of possible error-handling strategies, that can be called follow the names:

    1. Use exceptions for exceptional, rare, unexpected cases. When possibility that the throw-instruction is really called is low.
    2. Use std::expected for errors that are expected

    Sometimes it might mean that both ways are used in a single function - like the function returns std::excpected<T, E> for Error that is expected, but the function is not marked as noexcept because it can throw in some very rare cases. But if your established error-strategy is that functions returning std::expected<T,E> will never throw - then you need to have this "unexpected" errors be a variant of E.


    When applying this strategy to the question case, then std::expected should be selected, unless the input string is already validated according to your design - so, then the errors in parsing are not expected - so: exceptions. But most probably errors will be not totally unexpected - so std::expected. If the function can be noexcept or noexcept(false) - then this is really something that depends on its implementation:

    std::expected<int, parse_error> parse_int(std::string_view str) noexcept {
        if (str.empty()) {
            return std::unexpected(parse_error::empty_string);
        }
        /* ... */ // Here, if exceptions can happen, but are rare - you should not add `noexcept` to this function signature
        if (/* result too large */) {
            return std::unexpected(parse_error::out_of_range);
        }
        return result;
    }