Lesson 23 of 94
Pattern Matching in Depth
You have already used patterns in match, let, if let, and for. This lesson collects every kind of pattern in one place, from the simplest to the ones that make Rust code short and precise.
Rust's patterns cover a lot: ranges, slices, nested destructuring, bindings with @, and guaranteed exhaustiveness everywhere.
| Concept | Rust |
|---|---|
| Matching an enum variant | Shape::Circle(r) => |
| Destructuring a struct | Point { x, y } => |
| Guard condition | n if n > 10 => |
| Catch-all | _ => |
| Testing for one variant | if let Value::Str(s) = value |
| Ranges, slices, and bindings | Ranges 1..=9, slices [first, .., last], bindings n @ 1..=9 |
Where patterns appear
Patterns are not just for match. They appear in:
matcharms,letstatements (let (a, b) = pair;),- function and closure parameters (
fn f((x, y): (i32, i32))), if let,while let, andlet ... else,forloops (for (i, item) in ...).
1. Literal and multiple-value patterns
fn main() {
for code in [0, 1, 2, 3, 99] {
let meaning = match code {
0 => "success",
1 | 2 => "retryable error",
3 => "fatal error",
_ => "unknown",
};
println!("{code}: {meaning}");
}
}0: success
1: retryable error
2: retryable error
3: fatal error
99: unknown2. Range patterns for numbers and characters
fn classify(c: char) -> &'static str {
match c {
'a'..='z' => "lowercase",
'A'..='Z' => "uppercase",
'0'..='9' => "digit",
' ' | '\t' | '\n' => "whitespace",
_ => "other",
}
}
fn main() {
for c in ['q', 'Q', '7', ' ', '€'] {
println!("{c:?} is {}", classify(c));
}
}'q' is lowercase
'Q' is uppercase
'7' is digit
' ' is whitespace
'€' is other3. Binding a matched value with @
name @ pattern tests the pattern and also gives the matched value a name.
fn describe_age(age: u32) -> String {
match age {
0 => String::from("newborn"),
child @ 1..=12 => format!("child of {child}"),
teen @ 13..=19 => format!("teenager of {teen}"),
adult => format!("adult of {adult}"),
}
}
fn main() {
for a in [0, 7, 16, 42] {
println!("{}", describe_age(a));
}
}newborn
child of 7
teenager of 16
adult of 424. Destructuring tuples
fn main() {
let points = [(0, 0), (3, 0), (0, -2), (4, 5)];
for p in points {
let where_ = match p {
(0, 0) => String::from("origin"),
(x, 0) => format!("on the x-axis at {x}"),
(0, y) => format!("on the y-axis at {y}"),
(x, y) => format!("at ({x}, {y})"),
};
println!("{where_}");
}
}origin
on the x-axis at 3
on the y-axis at -2
at (4, 5)5. Destructuring structs
Fields can be matched against literals, bound to variables, renamed, or ignored with ...
struct Order {
id: u32,
country: &'static str,
total_cents: u64,
express: bool,
}
fn shipping_cents(o: &Order) -> u64 {
match o {
Order { express: true, country: "US", .. } => 1_500,
Order { express: true, .. } => 3_500,
Order { country: "US", total_cents: t, .. } if *t >= 5_000 => 0,
Order { country: "US", .. } => 500,
Order { total_cents, .. } => 1_000 + total_cents / 20,
}
}
fn main() {
let orders = [
Order { id: 1, country: "US", total_cents: 2_000, express: true },
Order { id: 2, country: "DE", total_cents: 2_000, express: true },
Order { id: 3, country: "US", total_cents: 7_500, express: false },
Order { id: 4, country: "US", total_cents: 1_000, express: false },
Order { id: 5, country: "JP", total_cents: 10_000, express: false },
];
for o in &orders {
println!("order {}: shipping {}", o.id, shipping_cents(o));
}
}order 1: shipping 1500
order 2: shipping 3500
order 3: shipping 0
order 4: shipping 500
order 5: shipping 1500total_cents: t binds the field under a new name; total_cents alone binds it under its own name.
6. Nested patterns
Patterns can nest as deeply as the data does.
enum Payment {
Card { network: &'static str, last4: &'static str },
Wallet(&'static str),
}
enum Status {
Paid(Payment),
Failed { reason: &'static str, retryable: bool },
Pending,
}
fn summary(s: &Status) -> String {
match s {
Status::Paid(Payment::Card { network: "amex", .. }) => String::from("paid with Amex"),
Status::Paid(Payment::Card { network, last4 }) => format!("paid with {network} ending {last4}"),
Status::Paid(Payment::Wallet(name)) => format!("paid with {name}"),
Status::Failed { retryable: true, reason } => format!("failed ({reason}), will retry"),
Status::Failed { reason, .. } => format!("failed permanently: {reason}"),
Status::Pending => String::from("pending"),
}
}
fn main() {
let statuses = [
Status::Paid(Payment::Card { network: "visa", last4: "4242" }),
Status::Paid(Payment::Card { network: "amex", last4: "0005" }),
Status::Paid(Payment::Wallet("Apple Pay")),
Status::Failed { reason: "timeout", retryable: true },
Status::Failed { reason: "card stolen", retryable: false },
Status::Pending,
];
for s in &statuses {
println!("{}", summary(s));
}
}paid with visa ending 4242
paid with Amex
paid with Apple Pay
failed (timeout), will retry
failed permanently: card stolen
pending7. Ignoring values: _, .., and _name
_matches anything and does not bind it...skips the remaining fields or elements._namebinds the value but silences the "unused variable" warning.
fn main() {
let record = (42, "ferris", "crab@example.com", true, 8);
let (id, _, email, ..) = record;
println!("{id} {email}");
let (first, .., last) = record;
println!("{first} {last}");
let _unused_for_now = record.3;
}42 crab@example.com
42 88. Match guards with several conditions
A guard can use any boolean expression, including the bound variables. A guard applies to the whole arm, including all | alternatives.
fn triage(cpu: u32, memory: u32, is_prod: bool) -> &'static str {
match (cpu, memory) {
(c, m) if is_prod && (c > 90 || m > 90) => "page on-call",
(c, m) if c > 90 || m > 90 => "open a ticket",
(c, _) | (_, c) if c > 75 => "watch",
_ => "ok",
}
}
fn main() {
println!("{}", triage(95, 40, true));
println!("{}", triage(95, 40, false));
println!("{}", triage(20, 80, true));
println!("{}", triage(20, 30, true));
}page on-call
open a ticket
watch
ok9. A variable name in a pattern *binds*, it does not compare
This is a common pattern mistake. A plain name in a pattern creates a new variable that matches anything. It does not compare against an existing variable with that name.
fn main() {
let expected_status = 200;
let actual = 404;
match actual {
expected_status => println!("matched! (actually bound {expected_status})"),
}
}matched! (actually bound 404)The compiler warns about this (irrefutable pattern / unreachable pattern when there are more arms). To compare with a variable, use a guard; to compare with a constant, use a const, whose uppercase name works in patterns:
const OK: u16 = 200;
fn main() {
let expected_status: u16 = 404;
for actual in [200, 404, 500] {
match actual {
OK => println!("{actual}: ok"),
s if s == expected_status => println!("{actual}: the expected status"),
_ => println!("{actual}: something else"),
}
}
}200: ok
404: the expected status
500: something else10. Refutable and irrefutable patterns
A pattern that always matches, like (a, b) for a tuple, is irrefutable and can be used with plain let. A pattern that might fail, like Some(x), is refutable. Using a refutable pattern with plain let is an error, because let has nowhere to go if it fails.
fn main() {
let maybe_port: Option<u16> = None;
let Some(port) = maybe_port;
println!("{port}");
}error[E0005]: refutable pattern in local binding
--> src/main.rs:3:9
|
3 | let Some(port) = maybe_port;
| ^^^^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
= note: the matched value is of type `Option<u16>`
help: you might want to use `let...else` to handle the variant that isn't matched
|
3 | let Some(port) = maybe_port else { todo!() };
| ++++++++++++++++Use if let, let ... else, or match for refutable patterns.
11. if let ... else if let ... else
enum Input {
Key(char),
Click(i32, i32),
Scroll(i32),
}
fn main() {
let inputs = [Input::Scroll(-3), Input::Key('q'), Input::Click(5, 9)];
for input in &inputs {
if let Input::Key(c) = input {
println!("key {c}");
} else if let Input::Click(x, y) = input {
println!("click {x},{y}");
} else {
println!("something else");
}
}
}something else
key q
click 5,912. if let chains with &&
Since the 2024 edition, if let can be combined with other conditions and other let patterns using &&. This removes a lot of nesting.
fn main() {
let raw_port: Option<&str> = Some("8443");
let use_tls = true;
if let Some(text) = raw_port
&& let Ok(port) = text.parse::<u16>()
&& use_tls
&& port != 80
{
println!("starting TLS listener on {port}");
} else {
println!("using defaults");
}
}starting TLS listener on 844313. while let for loops that end on a pattern
while let keeps looping as long as the pattern matches. It is ideal for draining a stack or a queue.
fn main() {
let mut pending = vec!["compile", "test", "package"];
while let Some(step) = pending.pop() {
println!("running {step}");
}
println!("done, {} left", pending.len());
}running package
running test
running compile
done, 0 left14. let ... else keeps the happy path flat
fn parse_header(line: &str) -> String {
let Some((name, value)) = line.split_once(':') else {
return format!("malformed: {line:?}");
};
let [first, ..] = name.trim().as_bytes() else {
return String::from("empty header name");
};
format!("{} (starts with {:?}) = {}", name.trim(), *first as char, value.trim())
}
fn main() {
println!("{}", parse_header("Content-Type: text/html"));
println!("{}", parse_header("no colon here"));
println!("{}", parse_header(" : value"));
}Content-Type (starts with 'C') = text/html
malformed: "no colon here"
empty header name15. matches! for a quick yes/no
#[derive(Debug)]
enum Role {
Admin,
Editor { sections: u8 },
Viewer,
}
fn main() {
let roles = [Role::Viewer, Role::Editor { sections: 3 }, Role::Admin, Role::Editor { sections: 0 }];
for r in &roles {
let can_write = matches!(r, Role::Admin | Role::Editor { sections: 1.. });
println!("{:?}: can write = {can_write}", r);
}
}Viewer: can write = false
Editor { sections: 3 }: can write = true
Admin: can write = true
Editor { sections: 0 }: can write = false1.. is a half-open range pattern: "1 or more".
16. Slice patterns
Slices and arrays can be matched by shape. rest @ .. binds the middle part as a sub-slice.
fn summarize(values: &[i32]) -> String {
match values {
[] => String::from("empty"),
[x] => format!("just {x}"),
[a, b] => format!("a pair: {a} and {b}"),
[first, rest @ ..] if rest.len() > 3 => format!("{first} and {} more", rest.len()),
[first, .., last] => format!("from {first} to {last}"),
}
}
fn main() {
println!("{}", summarize(&[]));
println!("{}", summarize(&[7]));
println!("{}", summarize(&[1, 2]));
println!("{}", summarize(&[1, 2, 3]));
println!("{}", summarize(&[1, 2, 3, 4, 5, 6]));
}empty
just 7
a pair: 1 and 2
from 1 to 3
1 and 5 more17. Matching the start of a slice
fn is_png(bytes: &[u8]) -> bool {
matches!(bytes, [0x89, b'P', b'N', b'G', ..])
}
fn describe(path_parts: &[&str]) -> String {
match path_parts {
["api", "v1", rest @ ..] => format!("v1 API call: {}", rest.join("/")),
["api", version, ..] => format!("unsupported API version {version}"),
["static", file] => format!("static file {file}"),
_ => String::from("not found"),
}
}
fn main() {
println!("{}", is_png(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A]));
println!("{}", is_png(b"GIF89a"));
println!("{}", describe(&["api", "v1", "users", "7"]));
println!("{}", describe(&["api", "v2", "users"]));
println!("{}", describe(&["static", "logo.svg"]));
println!("{}", describe(&["admin"]));
}true
false
v1 API call: users/7
unsupported API version v2
static file logo.svg
not found18. Matching a String against literals
A String can't be matched against &str literals directly. Match on s.as_str() instead.
fn content_type(extension: &str) -> &'static str {
match extension.to_lowercase().as_str() {
"html" | "htm" => "text/html",
"css" => "text/css",
"js" | "mjs" => "text/javascript",
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
_ => "application/octet-stream",
}
}
fn main() {
for ext in ["HTML", "js", "JPEG", "exe"] {
println!("{ext}: {}", content_type(ext));
}
}HTML: text/html
js: text/javascript
JPEG: image/jpeg
exe: application/octet-stream19. Matching references: & patterns
When you match on a reference, a & in the pattern takes the reference apart and copies out the value (for Copy types).
fn main() {
let scores = vec![90, 45, 72];
for &s in &scores { // s: i32
print!("{} ", s + 1);
}
println!();
match scores.first() { // Option<&i32>
Some(&top) if top >= 90 => println!("top score {top} is excellent"),
Some(top) => println!("top score {top}"),
None => println!("no scores"),
}
}91 46 73
top score 90 is excellent20. Default binding modes
When you match a reference with a pattern that has no &, Rust automatically makes every binding a reference. This is why matching on &Enum or &Struct never moves anything.
struct Config {
name: String,
retries: u32,
}
fn main() {
let c = Config { name: String::from("worker"), retries: 3 };
let Config { name, retries } = &c; // name: &String, retries: &u32
println!("{name} {retries}");
println!("c is untouched: {} {}", c.name, c.retries);
let mut m = Config { name: String::from("api"), retries: 0 };
let Config { name, retries } = &mut m; // name: &mut String, retries: &mut u32
name.push_str("-v2");
*retries += 5;
println!("{} {}", m.name, m.retries);
}worker 3
c is untouched: worker 3
api-v2 521. The ref keyword
Before default binding modes existed, you wrote ref in a pattern to bind by reference instead of by move. You will still see it in older code, and it is sometimes needed when matching on an owned value that you don't want to move.
fn main() {
let maybe_name: Option<String> = Some(String::from("Ferris"));
match maybe_name {
Some(ref name) => println!("hello {name}"), // borrow, don't move
None => println!("hello stranger"),
}
println!("still usable: {:?}", maybe_name);
}hello Ferris
still usable: Some("Ferris")Without ref, Some(name) would move the String out of maybe_name. Matching on &maybe_name is the modern way to write the same thing.
22. Patterns in function parameters
fn distance((x1, y1): (f64, f64), (x2, y2): (f64, f64)) -> f64 {
((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt()
}
struct Size {
width: u32,
height: u32,
}
fn aspect(&Size { width, height }: &Size) -> f64 {
width as f64 / height as f64
}
fn main() {
println!("{}", distance((0.0, 0.0), (3.0, 4.0)));
println!("{:.3}", aspect(&Size { width: 1920, height: 1080 }));
}5
1.77823. Patterns in for loops
struct Sale {
region: &'static str,
amount: u32,
}
fn main() {
let sales = [
Sale { region: "north", amount: 120 },
Sale { region: "south", amount: 340 },
];
for (rank, Sale { region, amount }) in sales.iter().enumerate() {
println!("#{} {region}: {amount}", rank + 1);
}
let pairs = vec![("a", 1), ("b", 2)];
for &(letter, number) in &pairs {
println!("{letter}{number}");
}
}#1 north: 120
#2 south: 340
a1
b224. Exhaustiveness across several values
The compiler checks exhaustiveness for any combination, not just single enums. Matching two booleans needs all four cases covered.
fn access(logged_in: bool, is_admin: bool) -> &'static str {
match (logged_in, is_admin) {
(false, _) => "please log in",
(true, true) => "admin dashboard",
(true, false) => "user dashboard",
}
}
fn main() {
println!("{}", access(false, true));
println!("{}", access(true, true));
println!("{}", access(true, false));
}please log in
admin dashboard
user dashboard25. Realistic example: rock, paper, scissors
#[derive(Debug, Clone, Copy, PartialEq)]
enum Hand {
Rock,
Paper,
Scissors,
}
#[derive(Debug)]
enum Outcome {
Win,
Lose,
Draw,
}
fn play(me: Hand, them: Hand) -> Outcome {
use Hand::*;
match (me, them) {
(a, b) if a == b => Outcome::Draw,
(Rock, Scissors) | (Paper, Rock) | (Scissors, Paper) => Outcome::Win,
_ => Outcome::Lose,
}
}
fn main() {
let rounds = [(Hand::Rock, Hand::Scissors), (Hand::Rock, Hand::Paper), (Hand::Paper, Hand::Paper)];
for (me, them) in rounds {
println!("{:?} vs {:?}: {:?}", me, them, play(me, them));
}
}Rock vs Scissors: Win
Rock vs Paper: Lose
Paper vs Paper: Draw26. Realistic example: an HTTP router
Splitting the path into segments and matching on (method, segments) gives a readable router with no regular expressions.
fn route(method: &str, path: &str) -> String {
let segments: Vec<&str> = path.trim_matches('/').split('/').filter(|s| !s.is_empty()).collect();
match (method, segments.as_slice()) {
("GET", []) => String::from("home page"),
("GET", ["users"]) => String::from("list users"),
("POST", ["users"]) => String::from("create user"),
("GET", ["users", id]) => format!("show user {id}"),
("DELETE", ["users", id]) => format!("delete user {id}"),
("GET", ["users", id, "orders", rest @ ..]) => format!("orders of user {id}, filter {:?}", rest),
(_, ["users", ..]) => String::from("405 method not allowed"),
_ => String::from("404 not found"),
}
}
fn main() {
let requests = [
("GET", "/"),
("GET", "/users"),
("POST", "/users/"),
("GET", "/users/42"),
("DELETE", "/users/42"),
("GET", "/users/42/orders/2026/09"),
("PUT", "/users/42"),
("GET", "/nope"),
];
for (m, p) in requests {
println!("{m:<6} {p:<28} -> {}", route(m, p));
}
}GET / -> home page
GET /users -> list users
POST /users/ -> create user
GET /users/42 -> show user 42
DELETE /users/42 -> delete user 42
GET /users/42/orders/2026/09 -> orders of user 42, filter ["2026", "09"]
PUT /users/42 -> 405 method not allowed
GET /nope -> 404 not found27. Realistic example: command-line argument parsing
#[derive(Debug)]
enum Cli {
Help,
Version,
Build { release: bool, target: Option<String> },
Run { args: Vec<String> },
Invalid(String),
}
fn parse(args: &[&str]) -> Cli {
match args {
[] | ["help"] | ["-h"] | ["--help"] => Cli::Help,
["--version"] | ["-V"] => Cli::Version,
["build"] => Cli::Build { release: false, target: None },
["build", "--release"] => Cli::Build { release: true, target: None },
["build", "--target", t] | ["build", "--release", "--target", t] => Cli::Build {
release: args.contains(&"--release"),
target: Some(t.to_string()),
},
["run", "--", rest @ ..] => Cli::Run { args: rest.iter().map(|s| s.to_string()).collect() },
[other, ..] => Cli::Invalid(format!("unknown command '{other}'")),
}
}
fn main() {
let inputs: [&[&str]; 7] = [
&[],
&["--version"],
&["build", "--release"],
&["build", "--release", "--target", "wasm32"],
&["run", "--", "--port", "8080"],
&["deploy"],
&["build", "--target", "x86_64"],
];
for args in inputs {
println!("{:<45} {:?}", format!("{:?}", args), parse(args));
}
}[] Help
["--version"] Version
["build", "--release"] Build { release: true, target: None }
["build", "--release", "--target", "wasm32"] Build { release: true, target: Some("wasm32") }
["run", "--", "--port", "8080"] Run { args: ["--port", "8080"] }
["deploy"] Invalid("unknown command 'deploy'")
["build", "--target", "x86_64"] Build { release: false, target: Some("x86_64") }28. Realistic example: a lexer
Character patterns with ranges and guards turn text into tokens.
#[derive(Debug, PartialEq)]
enum Token {
Number(i64),
Ident(String),
Op(char),
LParen,
RParen,
}
fn lex(src: &str) -> Vec<Token> {
let chars: Vec<char> = src.chars().collect();
let mut tokens = Vec::new();
let mut i = 0;
while i < chars.len() {
match chars[i] {
' ' | '\t' => i += 1,
'(' => { tokens.push(Token::LParen); i += 1; }
')' => { tokens.push(Token::RParen); i += 1; }
op @ ('+' | '-' | '*' | '/' | '=') => { tokens.push(Token::Op(op)); i += 1; }
'0'..='9' => {
let start = i;
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
let text: String = chars[start..i].iter().collect();
tokens.push(Token::Number(text.parse().unwrap()));
}
c if c.is_alphabetic() || c == '_' => {
let start = i;
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
tokens.push(Token::Ident(chars[start..i].iter().collect()));
}
other => panic!("unexpected character {other:?}"),
}
}
tokens
}
fn main() {
for t in lex("total = (price_1 + 42) * qty") {
println!("{:?}", t);
}
}Ident("total")
Op('=')
LParen
Ident("price_1")
Op('+')
Number(42)
RParen
Op('*')
Ident("qty")op @ ('+' | '-' | ...) binds whichever operator matched. The unwrap() is safe because the characters were all checked to be digits.
29. Realistic example: normalizing a date in several formats
fn normalize(date: &str) -> Option<String> {
let parts: Vec<&str> = date.split(|c| c == '-' || c == '/' || c == '.').collect();
let nums: Vec<u32> = parts.iter().map(|p| p.parse().ok()).collect::<Option<Vec<u32>>>()?;
let (y, m, d) = match (parts.as_slice(), nums.as_slice()) {
([y, _, _], &[year, month, day]) if y.len() == 4 => (year, month, day), // 2026-09-27
([_, _, y], &[day, month, year]) if y.len() == 4 && date.contains('.') => (year, month, day), // 27.09.2026
([_, _, y], &[month, day, year]) if y.len() == 4 => (year, month, day), // 09/27/2026
_ => return None,
};
match (m, d) {
(1..=12, 1..=31) => Some(format!("{y:04}-{m:02}-{d:02}")),
_ => None,
}
}
fn main() {
for input in ["2026-09-27", "27.09.2026", "09/27/2026", "2026-13-01", "27/9", "tomorrow"] {
println!("{input:>11} -> {:?}", normalize(input));
}
} 2026-09-27 -> Some("2026-09-27")
27.09.2026 -> Some("2026-09-27")
09/27/2026 -> Some("2026-09-27")
2026-13-01 -> None
27/9 -> None
tomorrow -> None30. Realistic example: summarizing a batch of job results
Nested enums plus guards and @ bindings classify results precisely.
enum JobResult {
Success { duration_ms: u64 },
Failure { code: i32, attempts: u32 },
Skipped(&'static str),
}
fn main() {
let results = [
JobResult::Success { duration_ms: 120 },
JobResult::Success { duration_ms: 9_500 },
JobResult::Failure { code: 137, attempts: 1 },
JobResult::Failure { code: 1, attempts: 5 },
JobResult::Skipped("no changes"),
];
let (mut fast, mut slow, mut failed) = (0, 0, 0);
for r in &results {
match r {
JobResult::Success { duration_ms: d @ 0..=999 } => { fast += 1; println!("fast success in {d}ms"); }
JobResult::Success { duration_ms } => { slow += 1; println!("slow success in {duration_ms}ms"); }
JobResult::Failure { code: 137, .. } => { failed += 1; println!("killed: out of memory"); }
JobResult::Failure { code, attempts } if *attempts >= 5 => { failed += 1; println!("gave up after {attempts} attempts (exit {code})"); }
JobResult::Failure { code, .. } => { failed += 1; println!("failed with exit {code}, will retry"); }
JobResult::Skipped(why) => println!("skipped: {why}"),
}
}
println!("fast={fast} slow={slow} failed={failed}");
}fast success in 120ms
slow success in 9500ms
killed: out of memory
gave up after 5 attempts (exit 1)
skipped: no changes
fast=1 slow=1 failed=2Summary
- Patterns appear in
match,let,if let,while let,let ... else, function parameters, andforloops. - Patterns can be literals, ranges (
1..=9,'a'..='z'), alternatives (a | b), wildcards (_,..), and destructurings of tuples, structs, enums, arrays, and slices. name @ patternbinds while testing. Guards (if cond) add arbitrary conditions.- A bare name in a pattern binds, it never compares. Use a guard or a
const. - Plain
letneeds an irrefutable pattern; useif let,let ... else, ormatchfor refutable ones. if letchains (if let A = a && let B = b && cond) remove nesting.- Matching on a reference binds references automatically.
Next: Result & the ? Operator, Rust's replacement for exceptions.
