Skip to content

Option, not null

Rust has no null: a value that can be missing has the type Option<T>, which is Some(value) or None, and the compiler makes you handle both.

In Java, any reference can be null. Optional<T> exists, but it is a convention for return values. Fields, parameters and collection elements can still be null, and the type does not tell you.

Java

Optional<Customer> find(long id) {
return customers.stream().filter(c -> c.id() == id).findFirst();
}

Rust

pub fn find(&self, id: u64) -> Option<&Customer> {
self.customers.iter().find(|c| c.id == id)
}

The two look the same. The difference is what the rest of the language does. In Java, Customer can also be null, and Optional itself can be null. In Rust, a &Customer always points to a customer. The only way to say “maybe no customer” is Option<&Customer>, and the only way to get the customer out is to handle None.

C# nullable reference types (Customer?) are closer: the compiler warns when you use a nullable value without a check. In Rust it is an error, not a warning, and there is no way to switch it off.

Option<T> is a normal enum from the standard library:

  • Some(value): there is a value.
  • None: there is no value.

It is used everywhere: return values, struct fields (nickname: Option<String>), parameters, and collection elements. Option<&T> costs nothing extra at run time: it has the same size as a pointer, with None stored as the null address. So there is no performance reason to avoid it.

Like Optional, Option has methods that transform the value without an explicit check:

Java

String displayName(long id) {
return find(id)
.map(c -> c.nickname() != null ? c.nickname() : c.name()) // fields can still be null
.orElse("unknown customer");
}
Optional<String> referrerName(long id) {
return find(id)
.map(Customer::referrerId) // null becomes Optional.empty()
.flatMap(this::find)
.map(Customer::name);
}

Rust

pub fn display_name(&self, id: u64) -> String {
self.find(id)
.map_or("unknown customer", |c| {
c.nickname.as_deref().unwrap_or(&c.name)
})
.to_string()
}
pub fn referrer_name(&self, id: u64) -> Option<&str> {
self.find(id)
.and_then(|c| c.referrer_id) // `None` if there is no customer or no referrer
.and_then(|rid| self.find(rid))
.map(|r| r.name.as_str())
}
Rust Java Optional Meaning
map(f) map(f) Transform the value, if there is one.
and_then(f) flatMap(f) Call a function that also returns Option.
unwrap_or(x) orElse(x) The value, or x if None.
unwrap_or_else(f) orElseGet(f) The value, or the result of f().
unwrap_or_default() (none) The value, or the type’s default (0, "", empty Vec).
map_or(x, f) map(f).orElse(x) Transform, or use x.
ok_or(err) orElseThrow(...) Convert to Result: Some(v) to Ok(v), None to Err(err).
filter(p) filter(p) Keep the value only if p is true.
unwrap() get(), orElseThrow() The value, or panic.

In a function that returns Option, ? means “if this is None, return None now”. It replaces a chain of and_then calls:

pub fn referrer_name_with_q(&self, id: u64) -> Option<&str> {
let customer = self.find(id)?; // `None`: return `None` now
let referrer = self.find(customer.referrer_id?)?;
Some(&referrer.name)
}
  1. Line 1: The function returns Option, so ? can return None early.
  2. Line 2: No customer with this ID: return None. Otherwise customer is a plain &Customer.
  3. Line 3: Two ? on one line: referrer_id is Option<u64>, and find returns Option<&Customer>.
  4. Line 4: Wrap the result in Some. &referrer.name is a &String, and it converts to &str.

This version and the combinator version of referrer_name above do the same thing. Choose the one that is easier to read. For more than two steps, ? is usually clearer.

To do something only when there is a value, use if let. To leave early when there is no value, use let ... else (stable since Rust 1.65):

pub fn greeting(&self, id: u64) -> String {
if let Some(customer) = self.find(id) {
format!("Hello, {}", customer.name)
} else {
"Hello".to_string()
}
}
pub fn require_name(&self, id: u64) -> Result<&str, UnknownCustomer> {
let Some(customer) = self.find(id) else {
return Err(UnknownCustomer(id));
};
Ok(&customer.name)
}

let Some(customer) = ... else { return ... }; reads as “the customer must exist; otherwise leave now”. After the statement, customer is a plain value. This is the idiomatic replacement for Java’s if (x == null) return ...; guard clause.

  • The type says it. If a value can be missing, its type is Option<T>. If it cannot be missing, its type is T. There is no third case.
  • Return Option<&T> from lookups, so the caller decides whether to copy.
  • Combinators or ? for short chains, match or if let when each case has real work.
  • let ... else for guard clauses, so the main path is not indented.
  • ok_or or ok_or_else at the boundary where “missing” becomes an error, so the error says what was missing.
  • Empty collections instead of None. A function that returns a list returns an empty Vec when there is nothing, not None.
  • .unwrap() without a reason. A panic in production when the value is missing. See S02: unwrap() / expect() in non-test code.
  • is_some() followed by unwrap(). A translation of if (x != null) { x.get() }. Use if let Some(x) = .... Clippy reports it (unnecessary_unwrap).
  • Option<bool> with three states (Some(true), Some(false), None) and no comment that says what None means. Use an enum with named variants.
  • Option<Option<T>> without a clear reason. Sometimes it is correct (a field that is “not set” vs “set to nothing” in a patch request), but it needs a comment.
  • Option<Vec<T>> when an empty Vec means the same as None. Every caller must handle two kinds of “nothing”.
  • unwrap_or(expensive()): the argument is evaluated even when there is a value. Use unwrap_or_else(expensive), which calls the function only for None. Clippy can report it (or_fun_call, in the nursery group, which is off by default).
  • Sentinel values instead of Option: -1 for “no index”, "" for “no name”, 0 for “no ID”. The Java habit of magic values, in a language that has a type for it.

The task: get the nickname of a customer, or the name if there is no nickname, or an empty string if there is no customer.

  1. WorseJava null checks in Rust syntax: is_some() then unwrap(), twice, with early returns.

    pub fn nickname_or_name(dir: &Directory, id: u64) -> String {
    let customer = dir.find(id);
    if customer.is_some() {
    let c: &Customer = customer.unwrap();
    if c.nickname.is_some() {
    return c.nickname.clone().unwrap();
    }
    return c.name.clone();
    }
    String::new()
    }
    Run in Playground (opens the Rust Playground in a new tab)
  2. BetterOne chain: find, choose nickname or name, default to an empty string. No unwrap.

    pub fn nickname_or_name(dir: &Directory, id: u64) -> String {
    dir.find(id)
    .map(|c| c.nickname.as_ref().unwrap_or(&c.name).clone())
    .unwrap_or_default()
    }
    Run in Playground (opens the Rust Playground in a new tab)

The “worse” version is correct today. The risk is in the next change: an unwrap() is safe only because of a check that is three lines away. If someone moves or changes the check, the unwrap() panics, and the compiler does not warn. In the “better” version, there is no unwrap() to break.

And a lookup that returns a list:

  1. WorseNone and Some(vec![]) both mean "no tags", so every caller must handle both.

    pub fn tags_for(sku: &str) -> Option<Vec<String>> {
    if sku.starts_with("BK-") {
    Some(vec!["book".to_string()])
    } else {
    None
    }
    }
    Run in Playground (opens the Rust Playground in a new tab)
  2. BetterAn empty Vec means "no tags". One case, and Vec::new() does not allocate.

    pub fn tags_for(sku: &str) -> Vec<String> {
    if sku.starts_with("BK-") {
    vec!["book".to_string()]
    } else {
    Vec::new()
    }
    }
    Run in Playground (opens the Rust Playground in a new tab)

Option<Vec<T>> is correct when “not loaded” and “loaded, but empty” are different states that the caller must handle differently. Then a comment, or an enum, should say so.

  1. Can a &Customer be null in Rust?

    Show the answer

    No. A reference always points to a valid value. “Maybe no customer” is Option<&Customer>, and the compiler makes you handle None before you use it.

  2. What is the Java equivalent of and_then?

    Show the answer

    Optional.flatMap. It calls a function that itself returns an Option, and does not nest the result.

  3. An agent wrote if user.is_some() { let u = user.unwrap(); ... }. What do you suggest?

    Show the answer

    if let Some(u) = user { ... }. It does the check and the extraction in one step, and there is no unwrap() that can panic after a later change.

  4. A function returns Option<Vec<Order>>. What do you ask?

    Show the answer

    What None means that an empty Vec does not. If there is no difference, return Vec<Order>.

  5. What does ? do in a function that returns Option<T>?

    Show the answer

    If the value is None, the function returns None at once. If it is Some(v), the expression has the value v, and the function continues.