Skip to main content
  1. Posts/

Rust basics: control flow

·665 words·4 mins
Author
m58
0xm58.xyz
Table of Contents
Rust control flow is more than branching and loops. The important part is that many control-flow forms are expressions, which means they produce values.

That design shows up everywhere in day-to-day Rust. It makes code concise, but it also forces you to be explicit about the shapes of the values you return.

if returns a value
#

In Rust, if is an expression, not just a statement.

let n = 7;
let parity = if n % 2 == 0 { "even" } else { "odd" };

This works because both branches return the same type: &str.

If the branch types do not match, the compiler complains:

let flag = true;

// invalid: one branch is i32, the other is &str
// let value = if flag { 10 } else { "ten" };

That restriction is useful. It keeps conditionals predictable and prevents “anything goes” branches.

Blocks are expressions too
#

Any block can evaluate to its last expression:

let score = {
    let raw = 40;
    raw + 2
};

score becomes 42 because the final line has no semicolon. Add a semicolon and the block returns (), the unit type.

match is Rust’s main branching tool
#

match is exhaustive, which means every possible case must be handled.

let code = 404;

let message = match code {
    200 => "ok",
    404 => "not found",
    500 => "server error",
    _ => "unknown",
};

That exhaustiveness matters most with enums:

enum State {
    Draft,
    Published,
    Archived,
}

fn label(state: State) -> &'static str {
    match state {
        State::Draft => "draft",
        State::Published => "published",
        State::Archived => "archived",
    }
}

If you later add a new enum variant, the compiler points out every match that now needs updating.

if let for one interesting case
#

Sometimes full match is more than you need. if let is the compact form when you only care about one pattern.

let maybe_name = Some(String::from("Ada"));

if let Some(name) = maybe_name {
    println!("hello {name}");
}

Use match when all variants matter. Use if let when one branch is the whole point.

loop for repeated work until break
#

loop is an explicit infinite loop:

let mut retries = 0;

loop {
    retries += 1;

    if retries == 3 {
        break;
    }
}

Like if, loop can also return a value:

let answer = loop {
    break 42;
};

That pattern is handy when a loop is searching for something and should yield the result directly.

while for condition-based looping
#

Use while when you want to repeat work as long as a condition stays true.

let mut n = 3;

while n > 0 {
    println!("{n}");
    n -= 1;
}

This is straightforward, but in idiomatic Rust you often end up preferring for over while when iterating through collections.

for is the default loop for iteration
#

When you want to walk over a range or collection, for is usually the right tool.

for i in 0..3 {
    println!("{i}");
}

You can iterate arrays, vectors, and iterators the same way:

let names = ["Ada", "Grace", "Linus"];

for name in names {
    println!("{name}");
}

This avoids manual indexing and usually reads better than while.

A practical example
#

Here is one small example that uses several of these forms together:

fn first_even(values: &[i32]) -> Option<i32> {
    for value in values {
        if value % 2 == 0 {
            return Some(*value);
        }
    }

    None
}

This function:

  • iterates with for
  • branches with if
  • returns early when it finds a match
  • falls back to None when nothing matches

That is typical Rust: small, explicit control flow with types that make outcomes clear.

Summary
#

  • if, match, and blocks are expressions that can produce values.
  • match is exhaustive and becomes especially valuable with enums.
  • if let is a compact way to handle one pattern.
  • for is the usual choice for iteration, while loop and while cover lower-level cases.

Once you get used to control flow producing values, Rust code starts to feel much more consistent.

Related

Rust basics: functions and types

·598 words·3 mins
Rust functions are explicit at the boundary and flexible inside the body. You spell out parameter and return types, then let inference do most of the local work. That balance is one of Rust’s better design choices. Signatures stay readable, but the code inside them does not turn into annotation noise.

Rust basics: let, mut, and shadowing

·481 words·3 mins
Rust variables are really bindings. Once that clicks, let, mut, and shadowing stop feeling like syntax trivia and start feeling like design choices. Rust pushes you toward immutability by default. That is not just style. It reduces accidental state changes and makes data flow easier to follow.

Rust basics: structs and enums

·587 words·3 mins
Structs and enums are the core of Rust data modeling. Structs group fields together. Enums describe a value that can be one of several well-defined variants. If you are coming from languages where data modeling is mostly “objects everywhere”, Rust feels different at first. It prefers explicit data shapes plus separate behavior.