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:
- Clauses are evaluated strictly from left to right with short-circuiting.
- Variables bound in an earlier
let Pat = exprclause are immediately in scope for all subsequent clauses to the right (&& key == "AUTH" && let Ok(id) = val.parse()) as well as inside theifbody block! - If any boolean expression evaluates to
falseor anyletpattern fails to match, evaluation short-circuits immediately to theelsebranch (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 anelsefallback or looping inwhile let Some(item) = iter.next() && item.is_valid().