cswork / libraryOpen practice workspace

Articles

Rust Error Handling Interview Questions: Result, ?, and Error Boundaries

Practice Rust error handling interview questions with Result, question-mark propagation, source chains, typed recovery boundaries, and cleanup evidence.

A parser returns Result, but its caller replaces the error with a string and loses the source. Adding ? shortens the code without deciding whether the application should retry, return a client error, or exit. Another function tries to propagate a parse failure into its own error enum and cannot compile because the conversion was never defined.

For Rust error handling interview questions, separate representation, propagation, context, and recovery. Result models success or failure. The ? operator exits the current function on an error and uses the required conversion. It does not automatically choose a recovery policy, enrich diagnostic context, or make every failure safe to ignore.

These original exercises use the Rust book and standard-library contracts, not employer reports.

What does Result make explicit?

Result<T, E> has a success variant carrying T and an error variant carrying E. The standard-library Result reference defines its methods and transformations. Returning Result makes a potential failure visible in the function's contract rather than hiding it behind an unconditional successful return type.

Start with an original port parser returning Result<u16, ParseIntError>. Empty text, a negative value, or a number outside the u16 range can fail parsing. A numeric value that parses successfully may still violate the application's domain policy: port zero may be disallowed even though zero is a valid u16.

That difference separates representation validation from business validation. Do not let the presence of Result create an impression that every domain requirement is enforced. The error type and implementation must describe which failures the API actually detects.

A match expression makes the two paths visible. Use it first when explaining control flow, then introduce propagation syntax. Code brevity is easier to evaluate after the reader can say exactly what happens to a failed parse and whether subsequent work runs.

What does ? do on each path?

For a Result expression, ? extracts the success value or returns from the enclosing function with an appropriately converted error. The Rust book's recoverable-error chapter explains this propagation pattern; the operator reference supplies its language-level description.

This original helper adds one domain check after parsing:

fn parse_port(text: &str) -> Result<u16, PortError> {
    let port: u16 = text.parse()?;
    if port == 0 {
        return Err(PortError::Zero);
    }
    Ok(port)
}

If parsing fails, the zero check is not reached. If parsing succeeds with zero, the function returns its own domain error. If it succeeds with a permitted value, the function returns Ok. A ledger or counter can prove which later stage ran instead of relying on a broad “errors bubble up” description.

The operator returns from the function or relevant enclosing error-handling context, not from the entire process. A caller can inspect the returned error and choose another action. Propagation through several functions does not imply that the application has already decided to terminate.

Also distinguish Result and Option paths. Both can use ? in appropriate return contexts, but a missing Option value does not automatically become your custom Result error. Convert intentionally, for example with an operation that supplies the desired missing-value error.

Which conversion makes propagation compile?

If an inner operation returns ParseIntError but the outer function returns PortError, the propagation path needs a supported conversion. A From implementation is a common way to define it. The From trait documentation describes conversions and their intended contract.

Use this original error representation:

#[derive(Debug)]
enum PortError {
    Parse(std::num::ParseIntError),
    Zero,
}

impl From<std::num::ParseIntError> for PortError {
    fn from(error: std::num::ParseIntError) -> Self {
        Self::Parse(error)
    }
}

The implementation preserves the parse error rather than discarding it. Removing it supplies a useful compile-negative fixture: the parser's ? cannot invent a conversion merely because the outer enum contains a vaguely related variant.

Use map_err when a call-site translation needs operation-specific information or a different mapping. That decision belongs where the necessary context is available. A generic From conversion cannot infer which configuration file or field the current caller was reading.

Avoid a catch-all conversion that changes every failure into a misleading category. A timeout is not automatically invalid input, and an internal I/O failure is not automatically a missing user field. Convenient propagation should preserve distinctions callers need to recover safely.

What should Display and source communicate?

The Error trait integrates an error with debugging and display behavior and can expose an underlying source error. Display should convey a useful message; source preserves the causal relationship when one error wraps another.

For the port exercise, a contextual wrapper can say which configuration setting failed while its source returns the original PortError. PortError::Parse can in turn expose the ParseIntError. That chain helps a diagnostic tool distinguish the operation context from the lower-level cause.

Do not claim that Debug output is a stable public error schema. Debug is useful during development, but downstream callers should not parse its incidental formatting to decide whether to retry. If machine-readable distinctions are required, expose a structured error contract.

Keep secrets out of context. Logging an entire environment variable, credential-bearing URL, or request body can turn better diagnostics into data exposure. A setting name and safe operation identifier can provide useful context without copying sensitive input.

Why can early stringification be harmful?

Converting an error to a String can preserve a human-readable message but lose its typed variants and source relationship. That may be acceptable at a final presentation boundary. It is often premature inside a library where callers still need structured decisions.

Compare two original adapters. One returns a ConfigError containing a PortError and setting name. The other immediately calls to_string and returns a generic message. Ask whether a caller can distinguish the zero-port domain error from a parse failure without inspecting prose.

The structured adapter supplies a stable place for recovery logic. The string-only adapter shifts classification into fragile message parsing or prevents it entirely. This is an API consequence, not a claim that String errors are always forbidden in small scripts.

Also avoid adding the same context at every layer. Repeated “failed to process” messages can obscure rather than clarify the cause. Each boundary should contribute information it uniquely owns: file selection, configuration field, request operation, or top-level command.

How do library and application boundaries differ?

A library usually serves callers with different recovery policies. A typed error enum can communicate actionable distinctions without choosing a process exit strategy. Its public shape is part of compatibility: adding variants or exposing implementation details affects what callers can rely on.

An application boundary often needs a different task: report the error, choose an exit status, attach logging context, and decide what information may be shown to the user. It may use a more flexible error wrapper when typed recovery is no longer required. That is a scope choice, not a requirement to use a particular third-party crate.

For the original configuration flow, the parsing library should not call process::exit because one port is invalid. A CLI can return a failure status after presenting the configuration error. A server may reject startup or report an operation failure according to its lifecycle contract.

If an API returns a boxed trait object, state what typed information the caller retains and what requires downcasting. Convenient heterogeneous storage can be useful, but it does not automatically provide the same exhaustive matching contract as a domain enum.

When are unwrap, expect, and panic reasonable?

The Rust book's panic-or-not discussion distinguishes expected recoverable failures from violated assumptions. unwrap or expect can be appropriate in a constrained demonstration or for a genuinely established invariant, but accepting arbitrary user input makes parsing failure an expected path.

An original fixture with the literal string "8080" may use an assertion to test a known successful outcome. A production parser reading a user-controlled setting should return the error instead of panicking because the same operation happened to succeed in the fixture.

Explain the invariant that justifies expect. “This should never fail” is not a proof. Identify which earlier check establishes the condition and whether another thread, file change, or external operation can invalidate it before use.

Panic cleanup behavior also depends on configuration. Unwinding can run destructors for ordinary owned values; an aborting panic strategy does not provide the same unwinding path. Do not present panic as a universal cleanup guarantee or a substitute for a normal error channel.

Does propagation still release owned resources?

Returning early with an error normally ends the current scope, so owned values in that scope are dropped according to Rust's lifetime rules. That can make an error path cleaner than manually scattering release calls throughout a function. It does not mean every external transaction was rolled back successfully.

For the original parser fixture, add a small Drop ledger guard before a fallible operation. Check that both success and returned-error paths record the guard's drop. This establishes the fixture's scope behavior, not filesystem durability, remote cancellation, or database rollback semantics.

Some cleanup needs to report failure or perform asynchronous work. A destructor cannot automatically satisfy every such protocol. Provide an explicit operation when the caller must await or inspect cleanup, and treat Drop as a separate fallback or ownership mechanism.

An error boundary should therefore answer two questions: which values leave scope, and which external effects remain unresolved? Preserving the original failure is important, but so is explaining whether a partial write, request, or commit may already have occurred.

Build positive, negative, and recovery tests

The original test set covers a valid port, zero, malformed text, overflow, a preserved source chain, context naming the setting, and a drop ledger on an early error. A separate negative file omits From and expects the compiler to reject the ? expression.

These fixtures were executed using rustc 1.98.1, without third-party dependencies. The negative file is expected to fail; that result demonstrates the missing conversion rather than a broken testing environment. Actual outputs and diagnostics are saved with the article.

Next test the caller's policy independently. Invalid input should not trigger an unlimited retry loop. A transient transport failure may warrant a bounded retry, but that decision needs a separate operation contract. The mere fact that both values implement Error does not make them recoverable in the same way.

Practice the error boundary

Practice questionError-specific follow-up
Implement retry wrapper and interdependent validatorsSeparate invalid input from a retryable operation failure.
Design a REST API Abstraction LayerPreserve typed errors until the transport boundary.
Implement buffered file writer with concurrency supportExplain what may have been written before an error.
Extract stream prefix before stop tokenDistinguish normal stopping from a failed input operation.
Review and Improve Four AI-Assisted TypeScript Pull RequestsReject wrappers that discard source errors prematurely.

For Rust error handling interview questions, trace the returned value, required conversion, source chain, and caller decision. A shorter function is useful only if its failure contract remains understandable.

Sources and Further Reading

Research note: Official sources checked September 30, 2026. Original local correctness fixtures use rustc 1.98.1; no external I/O recovery, crate comparison, or production durability result is claimed.