Skip to content

Patterns with if let

The if let syntax in Rust provides a more concise way to handle values that match a specific pattern while ignoring the rest. It’s particularly useful when you only care about one specific case of a value, rather than having to handle all cases with a full match expression.

When to Use if let

The if let construct is most useful when:

  1. You only need to handle one specific pattern
  2. You don’t need exhaustive pattern matching
  3. The full match expression would be overly verbose

Basic if let Syntax

Here’s the basic syntax:

if let Pattern = expression {
// code to run when the pattern matches
} else {
// optional code to run when the pattern doesn't match
}

Comparing match and if let

Let’s see how if let compares to match when handling an Option<T>:

Using match:

match some_option {
Some(value) => {
println!("Got a value: {}", value);
},
None => (), // Do nothing for None
}

Using if let:

if let Some(value) = some_option {
println!("Got a value: {}", value);
}

The if let version is more concise when you only care about the Some case.

JavaScript Analogies

In JavaScript, similar patterns might look like:

// JavaScript with optional chaining and nullish coalescing
const value = someObject?.value;
if (value !== undefined && value !== null) {
console.log(`Got a value: ${value}`);
}
// Or using destructuring with defaults
const { value } = someObject || {};
if (value) {
console.log(`Got a value: ${value}`);
}

Working with Options

The if let syntax is commonly used with Option<T>:

fn find_user(id: u32) -> Option<User> {
// Implementation details...
}
// Using if let
if let Some(user) = find_user(123) {
println!("Found user: {}", user.name);
} else {
println!("User not found");
}

JavaScript equivalent:

function findUser(id) {
// Implementation details...
return user || null;
}
const user = findUser(123);
if (user) {
console.log(`Found user: ${user.name}`);
} else {
console.log("User not found");
}

Working with Results

Similarly, if let works well with Result<T, E>:

if let Ok(num) = "42".parse::<i32>() {
println!("Successfully parsed: {}", num);
} else {
println!("Failed to parse");
}

JavaScript equivalent:

try {
const num = parseInt("42", 10);
if (!isNaN(num)) {
console.log(`Successfully parsed: ${num}`);
} else {
console.log("Failed to parse");
}
} catch (error) {
console.log("Failed to parse");
}

Pattern Matching in if let

if let supports all the same patterns as match:

// With structs
if let Point { x: 0, y } = point {
println!("Point is on the y-axis at {}", y);
}
// With enums and nested patterns
if let Message::ChangeColor(Color::Rgb(r, g, b)) = message {
println!("Changing color to RGB({}, {}, {})", r, g, b);
}
// With ranges
if let 1..=5 = value {
println!("Value is between 1 and 5");
}

Multiple Patterns with if let

In Rust 2021 and later, you can use | to match multiple patterns in an if let:

if let Some(1) | Some(2) | Some(3) = option_value {
println!("Value is 1, 2, or 3");
}

JavaScript might use the includes method:

const value = optionValue;
if (value !== null && [1, 2, 3].includes(value)) {
console.log("Value is 1, 2, or 3");
}

Combining if let with else if

You can chain if let statements:

if let Some(value) = some_option {
println!("first option: {}", value);
} else if let Some(value) = another_option {
println!("second option: {}", value);
} else {
println!("No options had values");
}

JavaScript equivalent:

if (someOption != null) {
console.log(`first option: ${someOption}`);
} else if (anotherOption != null) {
console.log(`second option: ${anotherOption}`);
} else {
console.log("No options had values");
}

Combining if let with Other Conditions

You can combine if let with additional conditions:

if let Some(value) = option && value > 10 {
println!("Got a value greater than 10: {}", value);
}

JavaScript equivalent:

if (option != null && option > 10) {
console.log(`Got a value greater than 10: ${option}`);
}

Using if let in Control Flow

if let is an expression, so it can be used in control flow:

let result = if let Some(value) = option {
value * 2
} else {
0
};

JavaScript equivalent:

const result = option != null ? option * 2 : 0;

Working with while let

Rust also has a while let pattern that keeps looping as long as a pattern matches:

let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
// Pop values off the stack while it's not empty
while let Some(value) = stack.pop() {
println!("Popped: {}", value);
}

JavaScript equivalent:

const stack = [1, 2, 3];
// Pop values off the stack while it's not empty
while (stack.length > 0) {
const value = stack.pop();
console.log(`Popped: ${value}`);
}

Destructuring with if let

if let can be used for destructuring complex data:

struct Point {
x: i32,
y: i32,
}
let point = Point { x: 0, y: 10 };
if let Point { x: 0, y } = point {
println!("Point is on the y-axis at {}", y);
}

JavaScript equivalent:

const point = { x: 0, y: 10 };
const { x, y } = point;
if (x === 0) {
console.log(`Point is on the y-axis at ${y}`);
}

Nested Destructuring

if let can perform nested destructuring:

enum Color {
Rgb(u8, u8, u8),
Hsv(u8, u8, u8),
}
enum Message {
ChangeColor(Color),
}
let msg = Message::ChangeColor(Color::Rgb(0, 160, 255));
if let Message::ChangeColor(Color::Rgb(r, g, b)) = msg {
println!("Changing color to RGB({}, {}, {})", r, g, b);
}

JavaScript equivalent:

const msg = {
type: 'ChangeColor',
color: { type: 'Rgb', values: [0, 160, 255] }
};
if (msg.type === 'ChangeColor' && msg.color.type === 'Rgb') {
const [r, g, b] = msg.color.values;
console.log(`Changing color to RGB(${r}, ${g}, ${b})`);
}

The let else Pattern

In Rust 1.65 and later, the let else pattern was introduced, which allows for early returns when a pattern doesn’t match:

fn process_age(age: Option<u32>) -> u32 {
let Some(age) = age else {
return 0; // Return early if age is None
};
age + 1 // Process the age
}

JavaScript equivalent:

function processAge(age) {
if (age == null) {
return 0; // Return early if age is null or undefined
}
return age + 1; // Process the age
}

Using if let for Error Handling

if let is useful for concise error handling with Result:

fn process_file(path: &str) -> Result<String, std::io::Error> {
let contents = std::fs::read_to_string(path)?;
if let Ok(parsed_data) = serde_json::from_str(&contents) {
// Process parsed data
return Ok(format!("Processed: {}", parsed_data));
}
Ok("Failed to parse, but continuing with default".to_string())
}

JavaScript equivalent:

async function processFile(path) {
try {
const contents = await fs.promises.readFile(path, 'utf8');
try {
const parsedData = JSON.parse(contents);
// Process parsed data
return `Processed: ${parsedData}`;
} catch {
// Failed to parse, but continue
return "Failed to parse, but continuing with default";
}
} catch (error) {
throw error; // Rethrow file reading errors
}
}

When to Use if let vs match

Use if let when:

  1. You only care about one specific pattern
  2. You don’t need exhaustive pattern matching
  3. The syntax is clearer and more concise

Use match when:

  1. You need to handle multiple cases
  2. You want to ensure exhaustiveness (handling all possible cases)
  3. You need to handle several complex patterns

Combining if let, else if let, and else

You can create complex branching logic:

if let Some(value) = first_option {
println!("First option has value: {}", value);
} else if let Some(value) = second_option {
println!("Second option has value: {}", value);
} else if let Ok(result) = operation() {
println!("Operation succeeded with: {}", result);
} else {
println!("All patterns failed to match");
}

JavaScript equivalent:

if (firstOption != null) {
console.log(`First option has value: ${firstOption}`);
} else if (secondOption != null) {
console.log(`Second option has value: ${secondOption}`);
} else {
try {
const result = operation();
console.log(`Operation succeeded with: ${result}`);
} catch {
console.log("All patterns failed to match");
}
}

Comparison with JavaScript Optional Chaining

JavaScript’s optional chaining (?.) and nullish coalescing (??) operators provide a way to handle potentially null or undefined values:

// JavaScript
const name = user?.profile?.name ?? "Anonymous";
function greet(user) {
if (user?.isLoggedIn) {
console.log(`Hello, ${user.name}!`);
} else {
console.log("Hello, guest!");
}
}

Rust’s equivalent would use combinations of if let and .map() or .unwrap_or():

// Rust
let name = user
.and_then(|u| u.profile)
.and_then(|p| p.name)
.unwrap_or_else(|| "Anonymous".to_string());
fn greet(user: Option<User>) {
if let Some(user) = user {
if user.is_logged_in {
println!("Hello, {}!", user.name);
return;
}
}
println!("Hello, guest!");
}

Rust’s Approach vs JavaScript’s Approach

Rust’s approach with Option, Result, and pattern matching:

  • Makes the potential absence of values explicit in the type system
  • Forces developers to handle all cases
  • Prevents null pointer exceptions at compile time
  • Uses expressions that return values, allowing for concise code

JavaScript’s approach with null/undefined and optional chaining:

  • More concise for simple cases
  • Can lead to runtime errors if not careful
  • Doesn’t enforce handling of edge cases
  • Often requires extra defensive coding

Summary

The if let syntax in Rust provides a concise way to handle pattern matching when you’re only interested in a single pattern. It’s especially useful for working with Option and Result types when you only care about the successful case.

While JavaScript doesn’t have direct equivalents to Rust’s pattern matching capabilities, features like destructuring, optional chaining, and nullish coalescing provide similar functionality for handling optional values.

Understanding if let helps JavaScript developers write more concise and expressive Rust code, especially when working with data that might be absent or operations that might fail.