04open 25 min

Error handling: Result, Option, and explicit absence

Translate from error returns and zero-value habits into Result, Option, and ?.

by the end of this lesson you can

  • Uses Option when no match is normal
  • Avoids inventing an error for ordinary absence
  • Keeps caller handling explicit

Overview

Go developers already expect explicit error paths, so Rust's error story is conceptually familiar. The deeper shift is that Rust also models absence explicitly with Option, and the type system prevents callers from mixing absence and failure casually.

In Go, you often

return (value, err) and sometimes rely on zero values or nil to mean absence.

In Rust, the common pattern is

to use Result<T, E> for failure, Option<T> for absence, and ? to propagate errors without hiding them.

why this difference matters

Rust sharpens a habit Go developers already have: failure and absence become different contracts, not merely different values.

Go

user, err := lookupUser(id)
if err != nil {
    return err
}
if user == nil {
    return ErrMissing
}

Rust

let user = lookup_user(id).ok_or(UserError::Missing)?;

Deeper comparison

Go version

cfg, err := loadConfig(path)
if err != nil {
    return nil, err
}
return cfg, nil

Rust version

fn load(path: &str) -> Result<Config, ConfigError> {
    let cfg = load_config(path)?;
    Ok(cfg)
}

Reflect

Why is it useful to separate 'not found' from 'operation failed' more aggressively than many Go APIs do?

what a strong answer notices

A strong answer mentions clearer contracts, less guesswork at the call site, and better modeling of ordinary absence.

Rewrite

Rewrite this Go function into Rust and choose Option or Result intentionally.

Rewrite this Go

func FirstAdmin(users []User) *User {
    for _, user := range users {
        if user.Admin {
            return &user
        }
    }
    return nil
}

what good looks like

  • Uses Option when no match is normal
  • Avoids inventing an error for ordinary absence
  • Keeps caller handling explicit

Practice

Design a Rust config loader that distinguishes parse failure from an optional field that is simply missing.

success criteria

  • Uses Result for actual failure
  • Uses Option where absence is expected
  • Explains how the caller handles both cases differently

Common mistakes

  • Using Result when Option is the clearer contract.
  • Treating None like a Rust version of any nil-ish value.
  • Flattening distinct error cases into strings too early.

takeaways

  • Rust sharpens a habit Go developers already have: failure and absence become different contracts, not merely different values.
  • A strong answer mentions clearer contracts, less guesswork at the call site, and better modeling of ordinary absence.
  • Uses Result for actual failure