DEV Community

Dev TNG
Dev TNG

Posted on

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
Enter fullscreen mode Exit fullscreen mode

Ruby's pretty much the same:

user = find_user(id)
email = user.email.upcase  # YOLO
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Or if you're fancy in Ruby:

email = user&.email&.upcase
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You write:

let email = user.map(|u| u.email.to_uppercase());
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

becomes:

let port = config.port.unwrap_or(8080);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In Rust:

let result = user
    .and_then(|u| get_profile(u.id))
    .and_then(|p| p.email);
Enter fullscreen mode Exit fullscreen mode

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())
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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(),
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

Quick Reference

What you want How 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 case match
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.

Top comments (0)