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?
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:
exceptions
for exceptional, rare, unexpected cases. When possibility that the throw-instruction is really called is low.std::expected
for errors that are expectedSometimes it might mean that both ways are used in a single function - like the function returns std::excpected<T, E>
for E
rror 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;
}