Loops #

Rust has three looping constructs — loop, while, and for — but the idiomatic Rust way of using them differs quite a bit from other languages. loop can return a value, making it useful as an expression rather than just a statement. for doesn’t work with manual indexes like for (int i = 0; i < n; i++) in C — it can only iterate over something that implements the IntoIterator trait. And Rust’s iterator ecosystem is far richer than just “looping through an array”: there’s map, filter, fold, zip, chain, and dozens of other adaptors that can all be chained without intermediate allocations. This article covers all three kinds of loops, how to choose among them, and how iterator methods can often replace explicit loops with code that’s more concise and easier to compose.

loop — Unbounded Looping #

loop runs a code block repeatedly without limit until an explicit break stops it. This isn’t an anti-pattern — there are many situations where an unconditioned loop is exactly right: a server that keeps waiting for connections, a game loop, or retry logic.

fn main() {
    let mut counter = 0;

    loop {
        counter += 1;

        if counter == 5 {
            break;
        }
    }

    println!("Counter stopped at: {}", counter); // 5
}

loop as an Expression #

This is a feature that doesn’t exist in many other languages: break can carry a value out of the loop, making it an expression that produces a result. Useful for retry logic or computations that need to loop until a condition is met.

fn main() {
    let mut upaya = 0;

    // loop returns a value via break
    let hasil = loop {
        upaya += 1;

        // Simulate an operation that needs repeated attempts
        if upaya == 3 {
            break upaya * 10; // this value becomes the result of the whole `loop`
        }
    };

    println!("Succeeded after {} attempts, result: {}", upaya, hasil); // 3, 30

    // Real-world use: retry with backoff
    let koneksi = loop {
        match coba_koneksi() {
            Ok(conn) => break conn,
            Err(e) => {
                eprintln!("Failed: {}, retrying...", e);
                std::thread::sleep(std::time::Duration::from_millis(100));
            }
        }
    };
    // `koneksi` is definitely Ok here — no need to unwrap again
    println!("Connected: {}", koneksi);
}

fn coba_koneksi() -> Result<String, &'static str> {
    // Simulation — assume success after a few attempts
    Ok(String::from("localhost:5432"))
}

Loop Labels and Labeled break #

In nested loops, a break without a label only stops the innermost loop. Use a label ('nama:) to break out of the outer loop from inside the inner loop:

fn main() {
    // ANTI-PATTERN: using a boolean flag to exit a nested loop
    let mut selesai = false;
    'outer_simulasi: for i in 0..5 {
        for j in 0..5 {
            if i == 2 && j == 3 {
                selesai = true;
                break;
            }
        }
        if selesai { break; }
    }

    // CORRECT: loop labels are much cleaner
    'pencarian: for i in 0..5 {
        for j in 0..5 {
            println!("Checking ({}, {})", i, j);
            if i == 2 && j == 3 {
                println!("Found at ({}, {})", i, j);
                break 'pencarian; // exits the outer loop directly
            }
        }
    }
    println!("Done");

    // Labeled loop with a return value
    let mut i = 0;
    let nilai = 'luar: loop {
        let mut j = 0;
        loop {
            if i + j == 10 {
                break 'luar i * j; // return a value from the labeled loop
            }
            j += 1;
        }
        i += 1;
    };
    println!("Value: {}", nilai);
}

while — Condition-Based Looping #

while evaluates the condition before every iteration and stops when the condition becomes false. It’s the right choice when the number of iterations isn’t known upfront and depends on state that changes.

fn main() {
    // Count until the condition is met
    let mut n = 1;
    while n < 100 {
        n *= 2;
    }
    println!("First above 100: {}", n); // 128

    // Countdown
    let mut hitungan = 5;
    while hitungan > 0 {
        print!("{}... ", hitungan);
        hitungan -= 1;
    }
    println!("Liftoff!");
}

When while Is Better Than loop #

fn main() {
    // ANTI-PATTERN: while emulated with loop + if + break
    let mut x = 0;
    loop {
        if x >= 10 { break; }
        x += 1;
    }

    // CORRECT: plain while expresses the intent more clearly
    let mut x = 0;
    while x < 10 {
        x += 1;
    }
    println!("x = {}", x);

    // ANTI-PATTERN: while used to iterate a collection with manual indexes
    let data = vec![10, 20, 30, 40, 50];
    let mut idx = 0;
    while idx < data.len() {
        println!("{}", data[idx]);
        idx += 1;
        // Easy to forget the increment, risk of off-by-one, risk of index out of bounds
    }

    // CORRECT: for is far safer and more idiomatic for iterating collections
    for elemen in &data {
        println!("{}", elemen);
    }
}

while with User Input #

A common while pattern is repeatedly asking for input until a valid condition is met:

use std::io;

fn baca_angka_positif() -> u32 {
    loop {
        let mut input = String::new();
        io::stdin().read_line(&mut input).expect("Failed to read");

        match input.trim().parse::<u32>() {
            Ok(n) if n > 0 => return n,
            Ok(_) => println!("Must be greater than 0, try again:"),
            Err(_) => println!("Not a valid number, try again:"),
        }
    }
}

for — Iterating over Collections #

for is the most frequently used loop in Rust. It works with any type that implements the IntoIterator trait — arrays, Vecs, ranges, Strings, HashMaps, and any type you define yourself.

fn main() {
    // Array
    let buah = ["apel", "mangga", "jeruk", "pisang"];
    for b in buah {
        print!("{} ", b);
    }
    println!();

    // Vec
    let angka = vec![1, 2, 3, 4, 5];
    for n in &angka {  // & so angka isn't moved
        print!("{} ", n);
    }
    println!();

    // Exclusive range
    for i in 0..5 {
        print!("{} ", i); // 0 1 2 3 4
    }
    println!();

    // Inclusive range
    for i in 1..=5 {
        print!("{} ", i); // 1 2 3 4 5
    }
    println!();

    // Reversed range with .rev()
    for i in (1..=5).rev() {
        print!("{} ", i); // 5 4 3 2 1
    }
    println!();
}

Three Vec Iteration Modes #

How you write the for determines whether elements are moved, borrowed, or mutably borrowed:

fn main() {
    let v = vec![String::from("a"), String::from("b"), String::from("c")];

    // 1. Move — ownership transfers, v can't be used after this
    for s in v {
        println!("{}", s);
    }
    // println!("{:?}", v); // error: v has been moved

    let v = vec![String::from("a"), String::from("b"), String::from("c")];

    // 2. Immutable borrow — v stays valid, elements can't be modified
    for s in &v {
        println!("{}", s); // s is of type &String
    }
    println!("v still exists: {:?}", v); // ✓

    let mut v = vec![1, 2, 3, 4, 5];

    // 3. Mutable borrow — each element can be modified
    for n in &mut v {
        *n *= 2; // must deref to change the value
    }
    println!("v after doubling: {:?}", v); // [2, 4, 6, 8, 10]
}

enumerate — Index and Value at Once #

fn main() {
    let makanan = ["nasi", "ayam", "sayur", "tempe"];

    // enumerate() produces pairs of (index, &element)
    for (i, item) in makanan.iter().enumerate() {
        println!("{}. {}", i + 1, item);
    }
    // 1. nasi
    // 2. ayam
    // 3. sayur
    // 4. tempe

    // ANTI-PATTERN: manual index tracking to get index and value
    let mut idx = 0;
    for item in &makanan {
        println!("{}. {}", idx + 1, item);
        idx += 1;
    }
    // More verbose, risk of forgetting the increment or off-by-one
}

zip — Iterating Two Collections Together #

fn main() {
    let nama = ["Budi", "Sari", "Joko"];
    let nilai = [85, 92, 78];

    // zip combines two iterators into an iterator of pairs
    for (n, v) in nama.iter().zip(nilai.iter()) {
        println!("{}: {}", n, v);
    }

    // zip stops when the shorter iterator runs out
    let a = [1, 2, 3, 4, 5];
    let b = [10, 20, 30]; // shorter
    for (x, y) in a.iter().zip(b.iter()) {
        println!("{} + {} = {}", x, y, x + y);
    }
    // Only 3 iterations — (1,10), (2,20), (3,30)
}

break and continue #

break stops the loop, continue skips the rest of the current iteration and moves on to the next one. Both can be used in loop, while, and for.

fn main() {
    // break — stop when the element is found
    let data = [3, 7, 2, 9, 1, 8, 5];
    let target = 9;
    let mut posisi = None;

    for (i, &val) in data.iter().enumerate() {
        if val == target {
            posisi = Some(i);
            break; // no need to continue
        }
    }
    println!("Target {} at position: {:?}", target, posisi);

    // continue — skip elements that don't qualify
    println!("Odd numbers:");
    for n in 0..10 {
        if n % 2 == 0 {
            continue; // skip evens
        }
        print!("{} ", n);
    }
    println!();

    // Labeled continue — skip an iteration of the outer loop
    'baris: for baris in 0..4 {
        for kolom in 0..4 {
            if kolom == 2 {
                continue 'baris; // move on to the next row
            }
            print!("({},{}) ", baris, kolom);
        }
        println!(); // never reached because continue 'baris skips it
    }
    println!();
}

Iterator Adaptors — A More Expressive Loop Alternative #

One of Rust’s greatest strengths is its iterator ecosystem. Instead of writing explicit loops, you can chain adaptors — map, filter, fold, take, skip, and dozens of others. The benefits: more concise, easier to compose, and often the intent is clearer to read.

flowchart LR
    S[Source\nVec / Array / Range] --> A1[map\ntransformation]
    A1 --> A2[filter\nfiltering]
    A2 --> A3[take / skip\nlimiting]
    A3 --> C[Consumer\ncollect / sum / for_each]
    C --> R[Final Result]

map — Transforming Every Element #

fn main() {
    let angka = vec![1, 2, 3, 4, 5];

    // ANTI-PATTERN: explicit loop for a simple transformation
    let mut kuadrat = Vec::new();
    for &n in &angka {
        kuadrat.push(n * n);
    }

    // CORRECT: map is more declarative — describes *what*, not *how*
    let kuadrat: Vec<i32> = angka.iter()
        .map(|&n| n * n)
        .collect();
    println!("{:?}", kuadrat); // [1, 4, 9, 16, 25]

    // Chain several transformations
    let hasil: Vec<String> = (1..=5)
        .map(|n| n * n)
        .map(|n| format!("{}²", n))
        .collect();
    println!("{:?}", hasil); // ["1²", "4²", "9²", "16²", "25²"]
}

filter — Filtering by Condition #

fn main() {
    let angka = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Filter only the evens
    let genap: Vec<&i32> = angka.iter()
        .filter(|&&n| n % 2 == 0)
        .collect();
    println!("Evens: {:?}", genap); // [2, 4, 6, 8, 10]

    // filter + map = filter_map (more efficient)
    let kata = vec!["42", "halo", "7", "dunia", "100"];

    let angka_valid: Vec<u32> = kata.iter()
        .filter_map(|s| s.parse::<u32>().ok()) // parse and filter in one step
        .collect();
    println!("Valid numbers: {:?}", angka_valid); // [42, 7, 100]
}

fold — Reducing to a Single Value #

fn main() {
    let angka = vec![1, 2, 3, 4, 5];

    // sum() is shorthand for fold for addition
    let jumlah: i32 = angka.iter().sum();
    println!("Sum: {}", jumlah); // 15

    // product() for multiplication
    let hasil: i32 = angka.iter().product();
    println!("Product: {}", hasil); // 120

    // fold() for arbitrary reductions
    let maks = angka.iter().fold(i32::MIN, |acc, &x| acc.max(x));
    println!("Maximum: {}", maks); // 5

    // Build a String from a collection
    let kalimat = ["Rust", "adalah", "bahasa", "yang", "cepat"];
    let gabung = kalimat.iter().fold(String::new(), |mut acc, &kata| {
        if !acc.is_empty() { acc.push(' '); }
        acc.push_str(kata);
        acc
    });
    println!("{}", gabung); // Rust adalah bahasa yang cepat

    // join() is more idiomatic for the case above
    let gabung2 = kalimat.join(" ");
    println!("{}", gabung2);
}

Other Commonly Used Adaptors #

fn main() {
    let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // take — take the first n elements
    let lima_pertama: Vec<_> = data.iter().take(5).collect();
    println!("First 5: {:?}", lima_pertama);

    // skip — skip the first n elements
    let setelah_lima: Vec<_> = data.iter().skip(5).collect();
    println!("After 5: {:?}", setelah_lima);

    // take_while — take while the condition holds
    let kecil: Vec<_> = data.iter().take_while(|&&x| x < 5).collect();
    println!("Less than 5: {:?}", kecil);

    // chain — combine two iterators
    let a = [1, 2, 3];
    let b = [4, 5, 6];
    let semua: Vec<_> = a.iter().chain(b.iter()).collect();
    println!("Chain: {:?}", semua);

    // any and all — check conditions
    println!("Any > 8: {}", data.iter().any(|&x| x > 8));  // true
    println!("All > 0: {}", data.iter().all(|&x| x > 0));      // true

    // count — count elements
    let jumlah_genap = data.iter().filter(|&&x| x % 2 == 0).count();
    println!("Number of evens: {}", jumlah_genap); // 5

    // find — find the first element satisfying the condition
    let pertama_genap = data.iter().find(|&&x| x % 2 == 0);
    println!("First even: {:?}", pertama_genap); // Some(2)

    // position — find the index of the first element
    let posisi = data.iter().position(|&x| x == 7);
    println!("Position of 7: {:?}", posisi); // Some(6)

    // min and max
    println!("Min: {:?}", data.iter().min()); // Some(1)
    println!("Max: {:?}", data.iter().max()); // Some(10)
}

Custom Iterators #

You can make any type iterable by implementing the Iterator trait. The only method that must be implemented is next() — all adaptors (map, filter, fold, etc.) become available automatically after that.

// A Fibonacci iterator that produces values without limit
struct Fibonacci {
    curr: u64,
    next: u64,
}

impl Fibonacci {
    fn baru() -> Self {
        Fibonacci { curr: 0, next: 1 }
    }
}

impl Iterator for Fibonacci {
    type Item = u64;

    fn next(&mut self) -> Option<u64> {
        let hasil = self.curr;
        let baru_next = self.curr + self.next;
        self.curr = self.next;
        self.next = baru_next;
        Some(hasil) // never None — an infinite iterator
    }
}

fn main() {
    // Take the first 10 Fibonacci numbers
    let fib_10: Vec<u64> = Fibonacci::baru()
        .take(10)
        .collect();
    println!("{:?}", fib_10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

    // Filter only the evens, take 5
    let fib_genap: Vec<u64> = Fibonacci::baru()
        .filter(|n| n % 2 == 0)
        .take(5)
        .collect();
    println!("{:?}", fib_genap); // [0, 2, 8, 34, 144]

    // Sum of Fibonacci numbers below 100
    let jumlah: u64 = Fibonacci::baru()
        .take_while(|&n| n < 100)
        .sum();
    println!("Sum of Fibonacci < 100: {}", jumlah); // 232
}

When to Choose Each Kind of Loop #

Use loop if:
  ✓ There's no clear initial condition — retry logic, server event loops
  ✓ You need to return a value from the loop via break
  ✓ The loop can stop from many different points

Use while if:
  ✓ There's a boolean condition checked at the start of every iteration
  ✓ The number of iterations is unknown and depends on external state
  ✓ Reading input until a valid condition is met

Use for if:
  ✓ Iterating a collection: Vec, array, HashMap, Range
  ✓ The number of iterations is known or bounded by the collection size
  ✓ This is the most common case — use for as the default

Use iterator adaptors (map/filter/fold) if:
  ✓ Simple transformation or reduction on a collection
  ✓ Composing several operations at once
  ✓ The result must be collected into a Vec or another type

Summary #

  • loop is an expressionbreak nilai returns a value from the whole loop block, useful for retry logic and computations until a condition is met.
  • Loop labels for nested loopsbreak 'label and continue 'label stop or continue a specific loop, far cleaner than boolean flags.
  • for with &koleksi for borrowingfor x in &v borrows v, for x in v moves ownership, for x in &mut v mutably borrows.
  • while for state-based conditions — use it when the number of iterations is unknown and depends on conditions that change at runtime.
  • Iterate collections with for, not while + index — safer (no risk of index out of bounds), more concise, and more idiomatic.
  • enumerate() for index + value, zip() for two collections at once — avoid manual index management.
  • Iterator adaptors for declarative transformationsmap, filter, fold, take, skip express what is done rather than how, and can be chained without intermediate allocations.
  • filter_map is more efficient than filter + map — one step for filtering and transforming at the same time.
  • Custom iterators only need next() implemented — all adaptors come for free via the Iterator trait once next() is implemented.

← Previous: Conditionals   Next: Functions →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact