Rust 2024 Edition

Let-Chains

Let-chains (if let Pat = expr && condition && let Pat2 = expr2) allow combining multiple let pattern-matching bindings and boolean expressions inside a single if or while condition using the logical AND operator (&&). To make let-chains sound and free of surprising lock-lifetime bugs, the Rust 2024 edition also overhauled the temporary drop scope rules for if let expressions (if_let_rescope).

Without Let-Chains (Deeply Nested Pyramid of `if let` + `if`):
  if let Some(header) = packet.split_once(':') {
      if header.0 == "AUTH" {
          if let Ok(token_id) = header.1.trim().parse::<u64>() {
              if token_id > 0 { ... }
          }
      }
  }

With Let-Chains (Flat, Left-to-Right Short-Circuiting Chain):
  if let Some((key, val)) = packet.split_once(':')
      && key == "AUTH"
      && let Ok(token_id) = val.trim().parse::<u64>()
      && token_id > 0
  { ... }

How Let-Chains Evaluate and Bind Variables

In a && chain inside if or while:

  1. Clauses are evaluated strictly from left to right with short-circuiting.
  2. Variables bound in an earlier let Pat = expr clause are immediately in scope for all subsequent clauses to the right (&& key == "AUTH" && let Ok(id) = val.parse()) as well as inside the if body block!
  3. If any boolean expression evaluates to false or any let pattern fails to match, evaluation short-circuits immediately to the else branch (where pattern-bound variables are not in scope).

Rust 2024 Temporary Drop Scope Fix (if_let_rescope)

In Rust 2021 and earlier, temporary values created inside the scrutinee of an if let (such as a MutexGuard or RefCell borrow in if let Some(x) = state.lock().unwrap().last().copied()) stayed alive all the way through the else block—frequently causing a runtime deadlock if the else branch also tried to lock state!

In Rust 2024, temporaries created inside an if let scrutinee are dropped before entering the else branch, eliminating Mutex and RefCell deadlocks in else branches:

Rust 2021 (`if let` temporary lived through `else` -> DEADLOCK in `else`!):
  if let Some(&val) = mutex.lock().unwrap().first() {
      use_val(val);
  } else {
      mutex.lock().unwrap().push(42); // ❌ Deadlocked in 2021 because lock was still held!
  }

Rust 2024 (`if_let_rescope` drops temporary before `else` -> No deadlock!):
  Temporary `MutexGuard` drops as soon as the `if` branch finishes (or before `else` starts)!
use std::sync::Mutex;

#[derive(Debug, PartialEq, Eq)]
pub struct AuthClaim {
    pub user: String,
    pub role: String,
    pub level: u32,
}

pub fn parse_admin_claim(line: &str) -> Option<AuthClaim> {
    if let Some((user, rest)) = line.split_once(':')
        && !user.trim().is_empty()
        && let Some((role, level_str)) = rest.split_once('=')
        && role.trim() == "admin"
        && let Ok(level) = level_str.trim().parse::<u32>()
        && level >= 5
    {
        Some(AuthClaim {
            user: user.trim().to_string(),
            role: role.trim().to_string(),
            level,
        })
    } else {
        None
    }
}

fn main() {
    let claim = parse_admin_claim("alice : admin = 10").unwrap();
    assert_eq!(claim.user, "alice");
    assert_eq!(claim.level, 10);
    assert_eq!(parse_admin_claim("bob : guest = 10"), None);

    // Demonstrate Rust 2024 `if let` temporary drop before `else`:
    let queue: Mutex<Vec<u32>> = Mutex::new(Vec::new());

    if let Some(&first) = queue.lock().unwrap().first() {
        println!("Queue already had: {first}");
    } else {
        // In Rust 2024, the temporary `MutexGuard` from `.lock()` in the `if let` condition
        // is already dropped before entering `else`, so locking here succeeds without deadlock!
        queue.lock().unwrap().push(100);
    }

    println!("Parsed claim: {claim:?}, queue after else: {:?}", *queue.lock().unwrap());
}
Parsed claim: AuthClaim { user: "alice", role: "admin", level: 10 }, queue after else: [100]

Combining let ... else Guards with if let Chains

  • Use let Some(x) = expr else { return ...; }; when you want to unwrap a required value at the top level of a function and exit early on failure.
  • Use let-chains (if let ... && ...) when branching conditionally with an else fallback or looping in while let Some(item) = iter.next() && item.is_valid().