RUST Contents

API Design with Result and Option

Design Rust APIs with Option and Result to model absence and failure explicitly, keep error boundaries clean, and return responses that are reliable and production-friendly.

On this page

Option vs Result: Model Reality, Not Convenience

In production systems, ambiguity is expensive. Rust forces you to encode uncertainty and failure explicitly through Option<T> and Result<T, E>. Good API design is choosing the right one, consistently, so callers can handle outcomes correctly without guesswork.

Use Option for Absence, Not Failure

Option<T> means "this value may or may not exist" and that is an expected outcome. It should not represent an error condition.

Common production examples:

  • Cache lookup miss
  • Optional query parameter
  • Record not found (sometimes)
fn find_cached(key: &str) -> Option> {
    None
}

Production rule: do not hide errors inside Option. If something can fail (I/O, parsing, DB), use Result.

Use Result for Operations That Can Fail

Result<T, E> means the operation can fail and you want to preserve failure information. This is the default for I/O, network calls, database operations, and validation.

fn parse_port(s: &str) -> Result {
    s.parse::().map_err(|_| "invalid port".to_string())
}

In production, errors must carry enough context to debug quickly.

The Most Common Boundary: Not Found

Whether "not found" should be Option or Result depends on the domain.

  • If "missing" is expected and normal: return Option
  • If "missing" is exceptional and should be logged/alerted: return Result with a specific error
#[derive(Debug)]
enum RepoError {
    Db(String),
}

fn get_user_by_id(id: u64) -> Result, RepoError> {
    Ok(None)
}

This pattern is common: the operation can fail (Result), but the record may not exist (Option).

Prefer Domain Errors Over String Errors

Strings are easy, but they are not structured. In production systems, structured errors help you map failures to HTTP responses, metrics, and logs.

#[derive(Debug)]
enum ServiceError {
    Validation(String),
    NotFound,
    Upstream(String),
}

Now you can do consistent error mapping:

  • Validation -> 400
  • NotFound -> 404
  • Upstream -> 502

Use the ? Operator to Propagate Errors

The ? operator keeps code readable and prevents error-handling noise. It propagates failures upward while preserving the error type.

fn load_config() -> Result {
    let contents = std::fs::read_to_string("config.toml")?;
    Ok(contents)
}

Production mindset: keep error handling close to boundaries. Propagate errors upward until you reach a boundary that can decide what to do (retry, log, return HTTP error).

Convert Errors at Boundaries

Internal layers should keep domain-specific errors. At boundaries (HTTP, CLI, gRPC), convert them into the format clients expect.

fn to_http_status(err: &ServiceError) -> u16 {
    match err {
        ServiceError::Validation(_) => 400,
        ServiceError::NotFound => 404,
        ServiceError::Upstream(_) => 502,
    }
}

Production rule: do not leak internal error variants directly to external clients.

Option Combinators: Keep Absence Logic Clean

Option provides combinators that keep absence logic readable.

fn normalize_name(input: Option<&str>) -> Option {
    input.map(|s| s.trim().to_lowercase())
}

This avoids nested if/else chains and keeps behavior explicit.

Result Combinators and Context

Result has similar combinators. Use them to add context before returning errors. Even without external crates, you can structure errors intentionally.

fn parse_timeout_ms(s: &str) -> Result {
    s.parse::()
        .map_err(|_| format!("timeout_ms must be a number, got={}", s))
}

Production rule: include the relevant input and boundary context, but never include secrets.

Return Types That Encourage Correct Handling

A production-friendly API makes the correct behavior easy and the incorrect behavior hard.

  • Return Option when absence is expected and callers can fall back safely
  • Return Result when failures must be handled explicitly
  • Combine them (Result<Option<T>>) when the operation can fail and the value may not exist

Production Pitfalls

  • Using Option for anything that can fail (hides failures)
  • Returning Result<T, String> everywhere (no structured handling)
  • Leaking internal errors to clients
  • Logging every error at every layer (duplicate noise)

Production Checklist

  • Option means absence, Result means failure
  • NotFound modeled intentionally (Option vs Result)
  • Use structured domain errors where it matters
  • Propagate with ? and convert at boundaries
  • Add context, but never leak secrets
  • Return types guide correct usage

Well-designed Result/Option usage is one of Rust's strongest production advantages. It turns ambiguous outcomes into explicit contracts and makes failure handling predictable and testable.