Articles
C++ Error-Handling Interview Questions: optional, variant, and expected
Practice C++ error-handling interview questions. Choose optional, variant, or expected, preserve error context, and test access and propagation boundaries.
C++ error-handling interview questions are really interface questions: what can happen, what information must survive, and what should the caller do next? optional<T> represents a value or absence. variant<A, B> represents one of several alternatives. expected<T, E> represents a value or an error. Those shapes overlap mechanically, but they communicate different contracts.
The strongest answer starts with the domain rather than a favorite container. Decide whether a missing value is normal, whether several successful representations exist, and whether failure needs an explanation. Then trace both the result and its resource ownership across the caller boundary.
This article uses C++17 for optional and variant, and C++23 for expected; library availability must be checked separately from the language flag.
C++ error-handling interview questions: choose the contract first
Suppose a configuration lookup finds no port setting. That may be a valid instruction to use a default. Now suppose the setting exists but contains eighty. That is malformed input. Returning the same empty optional for both cases makes the caller unable to distinguish normal absence from invalid configuration.
An optional is appropriate when absence is the entire relevant result: a cache miss, an unavailable measurement, or a search without a match. It does not automatically explain why a value is absent. Adding a separately mutable error field creates a synchronization problem between the two outputs.
A variant is appropriate when alternatives have their own domain meaning: a configuration value can be an integer, text, or a list. An alternative named Error can create a result convention, but the variant itself does not reserve an error branch. Your wrapper and naming must establish that interpretation.
An expected explicitly separates success from error. The error might contain a category, an input location, and safe diagnostic context. It need not be a string or an exception. The standard specifies these types separately; the distinctions here are interface-design guidance, not a rule that one type replaces all others. Optional, variant, expected
An original port-parser exercise
Define the contract before coding. An empty string means the setting is absent. A present value must be decimal digits representing a port from one through 65,535. Leading zeros are accepted. Whitespace, signs, and trailing characters are rejected rather than silently trimmed. These are this exercise's choices, not universal network-configuration rules.
The result type is:
enum class PortError { Malformed, OutOfRange };
using PortResult =
std::expected<std::optional<std::uint16_t>, PortError>;
There are three meaningful states: successful absence, successful presence, and failure. The nested optional is not redundant: expected distinguishes validity, while optional distinguishes whether a valid configuration supplied a value. If your application requires a port in every valid input, remove that optional and make empty input an error.
| Input | Result under this contract | Caller action |
|---|---|---|
| Empty string | Success with empty optional | Apply the documented default. |
443 or 00443 | Success containing 443 | Use the supplied port. |
eighty, 80tail, or 80 | Malformed error | Reject and report the invalid setting. |
0 or 65536 | Out-of-range error | Reject rather than wrap or clamp. |
| Very long all-digit number | Out-of-range error | Report numeric overflow without conversion wraparound. |
Parse into a sufficiently wide unsigned temporary using from_chars. Check both its error code and whether its returned pointer reached the end. A parser that accepts the prefix of 80tail has violated this contract even if numeric conversion succeeded.
First verify that every character is a decimal digit. That makes classification of signs and mixed strings unambiguous, including a long numeric prefix followed by letters. Then distinguish overflow from a successfully parsed number outside the allowed interval. Convert to the narrow integer only after range validation.
Trace the caller without collapsing states
A safe caller checks validity first, then presence:
auto result = parse_port(text);
if (!result) {
return report_port_error(result.error());
}
if (!result->has_value()) {
return use_default_port();
}
return use_port(**result);
This is a control-flow sketch: the three helper functions must have compatible return types in the real program. The first check protects access to the expected value. The second protects access to the optional value. Removing either check changes which states can be handled safely.
An interviewer may ask whether the nested dereference is too obscure. A named helper or a small domain wrapper can make the three states clearer. Keep the representation private when multiple call sites would otherwise duplicate policy or confuse valid absence with failure.
Do not replace the full result with result.value_or(...) merely to shorten the call site. A default can intentionally collapse failure into success, but that is a policy decision. In configuration code it may hide an operator's typo and connect the application to the wrong service.
Also define the default's ownership and timing. A default that performs I/O, allocates, or reads another mutable setting is not just a literal fallback. Evaluate it only on the branch that requires it, and test that an explicitly supplied valid port does not trigger the fallback action.
What happens on invalid value access?
Distinguish checked access from operations with preconditions. Calling an empty optional's value() throws bad_optional_access; requesting the wrong variant alternative with get throws bad_variant_access. An expected's value() on an error throws bad_expected_access<E> under its specified constraints. Expected observers
| Operation | Intended use | Failure boundary |
|---|---|---|
optional.value() | Checked extraction | Throws when disengaged. |
*optional | Extraction after proving presence | Presence is a precondition; not a recoverable fallback. |
get<T>(variant) | Checked alternative extraction | Throws if that alternative is not active. |
get_if<T>(&variant) | Probe an alternative | Returns a pointer or null. |
expected.value() | Checked success extraction | Throws on the error branch. |
expected.error() | Extract a known error | Error state must already be established. |
Do not demonstrate unchecked invalid access by running undefined behavior and interpreting whatever happens as the rule. Demonstrate checked exceptions in an isolated test, and use compile-time or control-flow reasoning for precondition violations.
variant also has a special valueless_by_exception state that can arise during certain throwing operations. It is not a normal extra business alternative and is not proof that every variant operation can lose its value. If the interviewer asks about it, identify the operation, exception guarantee, and recovery plan rather than generalizing from the type name.
How should expected errors propagate?
Explicit branching is often the clearest first implementation. Return unexpected(error) from a failed step, and invoke the next operation only after success. Preserve the category and useful context instead of replacing every failure with a generic message.
C++23 expected also provides monadic operations. and_then chains a callable returning another expected; transform changes the success value; or_else handles or converts an error; transform_error maps its error representation. Their constraints and value-category overloads matter. Expected monadic operations
In the port example, transform could map a valid optional port into a normalized configuration object. It must still define what valid absence means. and_then could call a later validation step, but that callable will receive the success payload, not automatically skip an empty optional nested inside it.
Test the failure path with a callback counter: malformed input must not invoke a success-only connection step. Test the success path separately, and verify that an error transformation keeps the information the caller needs. A beautiful chain that discards diagnostic context is not a better interface.
Monadic syntax does not create a transaction. If one step modifies a file and the next fails, earlier side effects still exist. Use RAII for resource cleanup and an explicit commit or compensation design where business state must be atomic.
Does expected eliminate exceptions?
No. Returning expected can make anticipated domain failures explicit, but allocation, constructors, callbacks, or surrounding library calls can still throw. “This function returns expected” is not equivalent to “this function cannot throw.” A noexcept declaration requires its own reasoning.
For a parsing boundary, invalid user input may belong in the error result while allocation failure follows the application's exception policy. For a service boundary, a narrow catch can translate a known library exception into a stable domain error. Do not catch everything and pretend all causes are recoverable validation failures.
Separate recoverable failures from broken invariants. A missing optional setting can be normal; corrupted internal state may indicate a programming defect. Returning a plausible default for both makes debugging harder and can conceal an unsafe operation. State which failures callers should retry, correct, ignore, or escalate.
Error context also has a privacy boundary. Preserve the field name and category, but avoid copying credentials or an entire configuration file into a public log. Rich errors are useful because they support decisions, not because they expose all underlying input.
Ownership and exception-safety follow-ups
Consider a stack's pop. Returning optional by value can express an empty stack without a sentinel. But moving its top element into the result may throw. Specify whether the element remains, becomes moved-from, or is removed before the operation completes. The return shape alone does not establish a strong exception guarantee.
For a variant holding a resource-owning object, ask what happens during alternative construction, assignment, and destruction. Which object owns the resource after a move? Does a failed replacement preserve the previous alternative? Answer from the actual operations and their exception guarantees.
For expected holding a move-only value, propagate without accidentally demanding a copy. The selected overload and value category determine whether the payload is borrowed, copied, or moved. An error containing a string_view also needs a backing lifetime; putting it in expected does not extend the referenced string's lifetime.
These follow-ups join interface design to resource management. A candidate who names the type but cannot explain mutation on failure has not yet defended the boundary. Write one success test, one ordinary error test, and one construction-or-move failure test where that failure is permitted.
A concise answer to take into the interview
Say: “I would first enumerate valid absence, successful alternatives, and failure. Optional communicates absence; variant communicates alternatives; expected communicates a success/error contract. I would then specify checked access, error propagation, lifetime, and mutation guarantees independently.”
Defend that answer with the port truth table and a caller trace. Change one requirement—empty input is now invalid, or an error must carry a location—and explain exactly which type and tests change.
Sources and Further Reading
- C++ working draft: optional objects
- C++ working draft: variants and alternative access
- C++ working draft: expected objects
- Expected value and error observers
- Expected monadic operations and constraints
Technical references checked October 3, 2026. Exercises are original; current working-draft wording may include changes beyond the stated baseline.