Skip to content
ExponaBLOG
RUST PROGRAMMING · DAY 011 / 100

Option instead of null: a practical guide for developers

Rust has no null.

Expona Admin6 min read

Why null is Rust's greatest absence

If you've come from JavaScript, Python, Java, or C++, one of your first surprises with Rust is that there is no null. No null pointer exceptions, no undefined is not a function errors, no billion-dollar mistakes. Instead, Rust gives you Option<T>, and it's one of the language's most elegant design decisions. Today we're exploring Option instead of null: a practical guide for developers who want to write reliable code that the compiler actually forces you to get right.

Understanding Option<T>: Some(value) or None

At its core, Option<T> is an enum with two variants: Some(T) when you have a value, and None when you don't. That's it. No hidden null lurking in the background; you declare it explicitly in your type signature.

rust
enum Option<T> {
    Some(T),
    None,
}

In practice, you'll rarely write this yourself—it's built into the standard library. But understanding this enum structure is fundamental. When a function might not return a value, you wrap the return type in Option:

rust
fn find_user_by_id(id: u32) -> Option<String> {
    if id == 0 {
        None
    } else {
        Some(format!("User {}", id))
    }
}

fn main() {
    let result = find_user_by_id(42);
    // result is Option<String>, not String
}

This shift—from hiding absence behind a null to declaring it in the type—changes everything. The compiler now knows that result might not contain a string, and it won't let you treat it as one without acknowledging that possibility.

The compiler forces you to handle None

This is where the safety comes in. You cannot accidentally ignore the possibility that a value is missing. Try this:

rust
fn main() {
    let name = find_user_by_id(42);
    println!("Hello, {}", name);  // Compiler error!
}

The compiler will reject this code immediately. name is an Option<String>, not a String, and you're trying to use it as if it's definitely a string. You must handle both cases.

In production systems—whether you're processing payment transactions, querying a database, or building an API—this forced explicitness prevents entire categories of bugs. We've all debugged a null reference error in production at 2 AM. Rust makes that impossible by design.

The simplest (but not always best) way to extract the value is pattern matching:

rust
fn main() {
    match find_user_by_id(42) {
        Some(user) => println!("Found: {}", user),
        None => println!("User not found"),
    }
}

Pattern matching is explicit and clear, but for everyday code, there are more ergonomic combinators.

map, and_then, and unwrap_or: practical combinators

Option provides a set of methods that let you work with the wrapped value without unwrapping it. These combinators are the backbone of clean, functional-style Rust code.

map: transform the value inside

If you have an Option<T> and want to apply a function to the value if it exists, use map:

rust
fn get_user_age(id: u32) -> Option<u32> {
    if id > 0 { Some(30) } else { None }
}

fn main() {
    let age = get_user_age(42);
    let birth_year = age.map(|a| 2024 - a);
    
    match birth_year {
        Some(year) => println!("Born around {}", year),
        None => println!("No age data"),
    }
}

If age is Some(30), then birth_year becomes Some(1994). If age is None, birth_year stays None. No manual null checks, no branching logic—just a declarative pipeline.

and_then: chain operations that return Option

and_then (also called flatMap in other languages) lets you chain operations where each step might fail. This is essential in real systems:

rust
fn get_user_by_id(id: u32) -> Option<String> {
    if id > 0 { Some(format!("User {}", id)) } else { None }
}

fn get_user_email(user: String) -> Option<String> {
    if user.contains("0") { None } else { Some(format!("{}@example.com", user)) }
}

fn main() {
    let email = get_user_by_id(42)
        .and_then(|user| get_user_email(user));
    
    match email {
        Some(addr) => println!("Email: {}", addr),
        None => println!("Could not retrieve email"),
    }
}

In a real API call scenario, get_user_by_id might query a database, and get_user_email might look up contact info. If either fails, you get None. If both succeed, you get Some(email). No intermediate null checks.

unwrap_or: provide a sensible default

When you actually do want to extract a value and have a fallback ready, unwrap_or is your tool:

rust
fn main() {
    let config_timeout = get_user_preference(42);
    let timeout_ms = config_timeout.unwrap_or(5000);
    
    println!("Request timeout: {}ms", timeout_ms);
}

If the user has set a preference, use it. Otherwise, use the default 5000ms. This is cleaner than ternary operators and more explicit than a null coalescing operator in other languages.

There's also unwrap_or_else if the default is expensive to compute:

rust
let timeout_ms = config_timeout.unwrap_or_else(|| calculate_default_timeout());

Avoid unwrap in production code

The unwrap method exists and it's tempting: it extracts the value or panics if you get None. In a learning exercise, fine. In production? Almost never.

rust
fn main() {
    let age = get_user_age(0);
    let years = age.unwrap();  // Will panic if age is None
    println!("{}", years);
}

A panic in production brings the whole process down. If you're running a web service and unwrap fails, your user sees a 500 error instead of a gracefully handled fallback. If you're in a backend job processing a queue, the job dies and needs retry logic.

Instead, use unwrap_or, map, and_then, or explicit pattern matching. Each has a reason:

  • unwrap_or or unwrap_or_else when you have a sensible default
  • map when you need to transform a value that might exist
  • and_then when chaining operations, each of which might fail
  • Pattern matching when you need different logic for Some and None

In a real e-commerce API, imagine fetching a user's shipping address:

rust
fn get_shipping_address(user_id: u32) -> Option<String> {
    // Database query
    Some("123 Main St".to_string())
}

fn calculate_shipping_cost(address: String) -> Option<f64> {
    // Looks up shipping zones
    Some(15.00)
}

fn main() {
    let user_id = 42;
    let cost = get_shipping_address(user_id)
        .and_then(calculate_shipping_cost)
        .unwrap_or(0.0);  // Free shipping if we can't determine cost
    
    println!("Shipping: ${}", cost);
}

If the user has no address on file, we gracefully default to free shipping instead of crashing.

Common mistakes

Mixing unwrap and production code: Treat every unwrap as a potential landmine. If you must use it (and you shouldn't in most cases), add a comment explaining why failure is impossible here, and be honest—it rarely is.

Nesting too many levels of Option: If you find yourself with Option<Option<T>>, you've probably missed a design. Use flatten or reconsider your function boundaries.

Forgetting that None is a value: None is not an error; it's the absence of a value. If something goes wrong, consider Result<T, E> instead (which we'll cover on a future day). Option is for "maybe there, maybe not." Result is for "success or failure with a reason."

Over-using match: Pattern matching is explicit and great, but map, and_then, and unwrap_or read more fluently. Use the right tool for the intent.

Key takeaways

  • Rust has no null. Option<T> explicitly represents "a value or nothing."
  • The compiler forces you to handle both Some and None. This eliminates entire categories of runtime bugs.
  • Use map to transform values, and_then to chain operations, and unwrap_or to provide defaults.
  • Avoid unwrap in production code. It panics, and panics bring services down.
  • Writing code that handles absence explicitly is safer, clearer, and ultimately faster than debugging null reference errors later.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles