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 #
loopis an expression —break nilaireturns a value from the wholeloopblock, useful for retry logic and computations until a condition is met.- Loop labels for nested loops —
break 'labelandcontinue 'labelstop or continue a specific loop, far cleaner than boolean flags.forwith&koleksifor borrowing —for x in &vborrows v,for x in vmoves ownership,for x in &mut vmutably borrows.whilefor state-based conditions — use it when the number of iterations is unknown and depends on conditions that change at runtime.- Iterate collections with
for, notwhile + 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 transformations —
map,filter,fold,take,skipexpress what is done rather than how, and can be chained without intermediate allocations.filter_mapis more efficient thanfilter+map— one step for filtering and transforming at the same time.- Custom iterators only need
next()implemented — all adaptors come for free via theIteratortrait oncenext()is implemented.