Skip to content

Option::is_some_and and let-else

Published 9 October 2026

What it does

Option::is_some_and returns true only if the option is Some and the value inside passes a predicate. None is always false.

pub fn is_some_and(self, f: impl FnOnce(T) -> bool) -> bool

The implementation is a two-arm match:

match self {
    None => false,
    Some(x) => f(x),
}

f runs at most once, so FnOnce is enough (same reasoning as map, see Revision 2026-10-02).

The three cases

let age: Option<u32> = Some(25);
assert!(age.is_some_and(|a| a >= 18));   // true

let age: Option<u32> = Some(12);
assert!(!age.is_some_and(|a| a >= 18));  // false: predicate fails

let age: Option<u32> = None;
assert!(!age.is_some_and(|a| a >= 18));  // false: it's None

What it replaces

// before
if let Some(a) = age { if a >= 18 { /* ... */ } }
matches!(age, Some(a) if a >= 18)
age.map_or(false, |a| a >= 18)

// now
age.is_some_and(|a| a >= 18)

Clippy flags the map_or(false, ...) form:

warning: this `map_or` can be simplified
   = note: `#[warn(clippy::unnecessary_map_or)]` on by default
help: use `is_some_and` instead

It consumes the Option

Takes self by value

For a Copy type like Option<u32> that’s invisible. For Option<String> the call moves the option, and it’s gone afterwards.

let name: Option<String> = Some("Karthik".to_string());
let _ = name.is_some_and(|s| s.starts_with('K'));
println!("{name:?}"); // error[E0382]: borrow of moved value: `name`

Borrow first with as_ref() (Option<&String>) or as_deref() (Option<&str>):

let name: Option<String> = Some("Karthik".to_string());
assert!(name.as_ref().is_some_and(|s| s.starts_with('K')));
assert!(!name.as_deref().is_some_and(|s| s.is_empty()));
println!("{name:?}"); // still usable

Examples where it reads well

Methods that already return Option<&T>, so no as_ref() needed:

let v = vec![1, 2, 3];
assert!(v.last().is_some_and(|&x| x == 3));
assert!(v.first().is_some_and(|x| *x == 1));

let words = vec!["apple", "", "kiwi"];
let first_long = words.iter().find(|w| w.len() > 4);
assert!(first_long.is_some_and(|w| w.starts_with('a')));

Passing a function path instead of a closure:

let input = "  hi ";
assert!(input.trim().chars().next().is_some_and(char::is_alphabetic));

A field check in an if:

if booking.start_odometer.is_some_and(|s| s > 0) {
    println!("trip started");
}

The counterpart: is_none_or

is_none_or (stable since Rust 1.82) is the “vacuously true” version: true if the option is None or the predicate holds.

fn within_limit(n: u32, limit: Option<u32>) -> bool {
    limit.is_none_or(|l| n <= l) // no limit, or within it
}

assert!(within_limit(5, None));
assert!(within_limit(5, Some(10)));
assert!(!within_limit(15, Some(10)));
self predicate is_some_and is_none_or
None — false true
Some(x) true true true
Some(x) false false false

Result has the same pair

is_ok_and and is_err_and. They also take self by value, so use as_ref() to keep the Result:

let r: Result<u32, String> = "42".parse::<u32>().map_err(|e| e.to_string());
assert!(r.as_ref().is_ok_and(|&n| n > 40));

let e: Result<u32, String> = Err("timeout".into());
assert!(e.as_ref().is_err_and(|m| m.contains("time")));

is_some_and, is_ok_and and is_err_and were stabilized in Rust 1.70.

Another way to deal with an Option without nesting. This looks odd the first time you see it:

let Some(ticket) = self.tickets.get(ticket_id) else {
    return false;
};
// `ticket` is in scope here, already unwrapped

This is let-else, stable since Rust 1.65. It’s a guard clause. Rust tries to match the pattern Some(ticket). If it matches, ticket is bound in the enclosing scope and execution continues. If it doesn’t, the else block runs.

The else block must diverge

It has to return, break, continue, panic!, or otherwise never fall through. Otherwise ticket would be unbound on the next line. The compiler enforces this:

error[E0308]: `else` clause of `let...else` does not diverge
  |
  |     let Some(n) = o else { println!("none"); };
  |                          ^^^^^^^^^^^^^^^^^^^^^ expected `!`, found `()`

What it replaces

// 1. match: verbose, the binding comes out through an expression
let ticket = match self.tickets.get(ticket_id) {
    Some(t) => t,
    None => return false,
};

// 2. if let: the happy path ends up nested inside the block
if let Some(ticket) = self.tickets.get(ticket_id) {
    // ...everything indented here
} else {
    return false;
}

let-else keeps the early exit at the top and the rest of the function flat. It’s Swift’s guard let.

Why not ? The function returns bool, not Option/Result, so there is nothing for ? to propagate into.

Any refutable pattern works

let Ok(n) = s.parse::<u32>() else { return Err(format!("bad number: {s}")) };

let [first, .., last] = slice else { return None }; // needs at least 2 elements

for ev in events {
    let Event::Click { x, y } = ev else { continue };
    // only clicks get here
}

is_some_and or let-else?

  • You only need a yes/no answer: opt.is_some_and(|x| ...).
  • You need the value for the rest of the function: let Some(x) = opt else { ... };.