So You're a Ruby/Python Dev Learning Rust's Option Type

Rust's Option type forces you to handle the "maybe it's there, maybe it's not" problem upfront using combinators like .map().unwrap_or(), and the ? operator—basically replacing the "oops, didn't check for nil" debugging session at 2am with compile errors you fix before running anything.

Look, I get it. You've been happily writing Ruby or Python for years, everything's fine, and then someone convinces you to try Rust. Suddenly the compiler is yelling at you about Option and you're like "just let me get the damn email address."

Let me save you some pain.

What You're Used To

Here's what we do in Python:

user = find_user(id)
email = user.email.upper()  # fingers crossed

Ruby's pretty much the same:

user = find_user(id)
email = user.email.upcase  # YOLO

This is fine until you forget to check and production goes down because NoneType object has no attribute 'email'. We've all been there. Multiple times. That's why you learned to do this:

email = user.email.upper() if user else None

Or if you're fancy in Ruby:

email = user&.email&.upcase

Works great! Except when you forget. Which happens more than we'd like to admit.

Rust's "Actually, Let's Not Do That" Approach

Rust doesn't have null. Instead you get Option<T>:

let user: Option<User> = find_user(id);

This is either:

  • Some(user) - we found something
  • None - nothing there

The big difference? The compiler won't let you pretend it's always Some. You have to handle both cases or it won't compile. Period.

At first this feels like the language is being difficult. After a few months you realize you haven't debugged a null pointer error in forever and it starts making sense.

The Combinators (Fancy Name, Simple Concept)

.map() - Do something if there's a value

Instead of:

email = user.email.upper() if user else None

You write:

let email = user.map(|u| u.email.to_uppercase());

If user is Some, run the function. If it's None, just return None. No crash, no fuss. The |u| part is just Rust's way of saying "for each user u" - it's like Python's lambda or Ruby's block.

.unwrap_or() - Fallback values

port = config.get('port') or 8080

becomes:

let port = config.port.unwrap_or(8080);

Pretty straightforward.

.and_then() - Chain operations

This one's useful when you're calling multiple things that could return None:

result = None
if user:
    profile = get_profile(user.id)
    if profile:
        result = profile.email

In Rust:

let result = user
    .and_then(|u| get_profile(u.id))
    .and_then(|p| p.email);

Each .and_then() only runs if the previous step returned Some. If anything's None, the whole chain stops and returns None.

The ? operator - This one's actually cool

Check this out:

fn get_user_email(id: u32) -> Result<String, Error> {
    let user = find_user(id)?;
    let email = user.email.ok_or(Error::NoEmail)?;
    Ok(email.to_uppercase())
}

That ? means "if this is None or an Error, stop here and return it. Otherwise unwrap the value and keep going." It's like Ruby's &. but it actually propagates the error up instead of just turning things into nil.

This is probably the most Rust thing you'll write. Once you get used to it, it's hard to go back.

Common Mistakes (I Made All of These)

Don't do this:

let user = find_user(id).unwrap();

That's basically the same as not checking for nil in Python. It'll crash if user is None. The compiler lets you do it, but don't.

Do this instead:

let user = find_user(id)?;  // let caller handle it
// or
let user = find_user(id).unwrap_or_default();  // sensible default
// or
match find_user(id) {
    Some(u) => handle_user(u),
    None => handle_missing(),
}

Use .expect("reason") instead of .unwrap() if you really think it should always be Some - at least you'll leave a note for future you:

let config = load_config().expect("config.toml must exist");

Quick Reference

What you wantHow to do it
Transform the value`.map(\
Chain Option-returning calls{% raw %}`.and_then(\
Use a default{% raw %}.unwrap_or(default)
Return None/Error early?
Different logic per casematch
Crash with a message (tests only).expect("message")

FAQ

Q: Why is this better than just checking for nil?

Because you can't forget. The compiler forces you to handle it. I know it seems annoying at first, but after you've been saved from a null pointer error for the 50th time, you start to appreciate it.

Q: What if I know for sure it's Some?

Use .expect() with a message explaining why you're sure. When (not if) it eventually panics, you'll thank yourself for the note.

Q: This is way more verbose than Python/Ruby

Yeah, kind of. But you're not writing more code, you're making the error handling visible. In Python/Ruby it's still there, you're just hoping you remembered to do it. Ask yourself: how many times have you shipped code that didn't properly check for None/nil?

Q: Can I just unwrap everything and move on?

You can, but then why are you using Rust? That's like buying a car with airbags and then disabling them. The whole point is catching this stuff before it becomes a production issue.

Real Talk

Coming from Ruby/Python, Rust's Option handling feels like overkill at first. You'll be annoyed. You'll complain about the compiler. You'll wonder why you can't just check if something is None like a normal person.

Then one day you'll realize you haven't debugged a NoneType error in months. You'll refactor some code and the compiler will catch all the places where you forgot to handle the new None case. You'll ship with confidence because if it compiles, those error paths are actually handled.

That's when it clicks.

The learning curve sucks, not gonna lie. But the compiler is basically doing the code review you'd normally catch in production. It's annoying in the way a good senior dev is annoying when they point out all the edge cases you missed.

Anyway, hope this helps. Go write some Rust. Make mistakes. Let the compiler yell at you. You'll get there.


Written by someone who absolutely tried to .unwrap()(cloudflare :) ) everything for the first two weeks and learned the hard way.

Previous Post Next Post