loop
loop
Level 2 — Control Flow & Data Structures An infinite loop; exit with
break(which can return a value).
1. Prerequisites
if/else— You almost always need anifstatement to decide when to stop the loop.- Expressions — Understanding how blocks of code can evaluate to a value.
2. Term Category
Rust-specific (mostly): While many languages use while(true) for infinite loops, Rust provides a dedicated loop keyword. Furthermore, Rust's loop is unique because it is an expression that can return a value via the break statement.
3. Explanation
(1) Design Motivation — "Why did we design this?"
Infinite loops are incredibly common in programming. A web server runs in an infinite loop listening for requests, a video game runs in an infinite rendering loop, and network requests often need to retry infinitely until they succeed.
Instead of forcing developers to write the slightly awkward while true { ... }, Rust provides the explicit loop keyword. This isn't just syntactic sugar—it actually helps the Rust compiler! When the compiler sees loop, it knows with 100% certainty that the code inside will run at least once and won't stop until it hits a break.
Because Rust loves Expressions, loop can also evaluate to a value. If you are looping specifically to calculate a result (like waiting for a user to type a valid number), you can hand that result directly to the break keyword. The entire loop block will then evaluate to that value, allowing you to assign it cleanly to a let variable.
(2) Reality Metaphor
A loop is like running on a treadmill.
Once you press start, you will keep running infinitely. You only stop when a specific condition occurs (e.g., you hit 5 miles, or you get too tired). At that point, you hit the big red break button to stop the machine.
Returning a value from a loop is like stepping off the treadmill and immediately handing your final calorie count (the value) to your fitness app (the variable).
(3) Rust Code Examples
Short Snippet
let mut counter = 0;
loop {
counter += 1;
if counter == 3 {
println!("Hit the limit! Stopping.");
break; // This exits the loop entirely.
}
}
Fuller Example
fn main() {
let mut retry_count = 0;
// We want to retry a fake network connection until it succeeds.
// Because `loop` is an expression, we can assign its result to `status`.
let status = loop {
retry_count += 1;
if retry_count < 5 {
println!("Connection failed, retrying...");
continue; // `continue` skips to the next iteration of the loop.
}
// When we finally succeed, we pass the "Success" string to `break`.
// This stops the loop AND returns "Success" to the `status` variable.
break "Success";
};
// Note the semicolon after the `loop` block!
// It is required because this was a `let` statement.
println!("Final connection status: {}", status);
}
4. Common Mistakes & Pitfalls
Mistake 1: Misunderstanding Loop Scoping and Lifecycle Rules
The mistake: Assuming Loop instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.
Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.
Incorrect:
fn get_ref() -> &str {
let s = String::from("loop_data");
&s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}
Fix:
fn get_string() -> String {
let s = String::from("loop_data");
s // Ownership of the String is transferred directly to the caller
}
Mistake 2: Mutating Loop State Without Exclusive Ownership or mut Borrowing
The mistake: Attempting to mutate data associated with Loop through an immutable reference &T or without specifying mut in variable declarations.
Why it's wrong: Rust's aliasing XOR mutability rule (&T for shared immutable access, &mut T for exclusive mutable access) prohibits mutating state through shared references unless interior mutability patterns (e.g. RefCell, Mutex) are explicitly used.
Incorrect:
fn update_val(data: &i32) {
// *data += 1; // ❌ Error E0594: cannot assign to `*data`, which is behind a `&` reference
}
Fix:
fn update_val(data: &mut i32) {
*data += 1; // Correct: exclusive mutable reference permits mutation
}
Mistake 3: Concurrent Access to Loop Across Threads Without Send / Sync Guards
The mistake: Sharing non-thread-safe Loop instances across OS threads via std::thread::spawn.
Why it's wrong: Types that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).
Incorrect:
use std::rc::Rc;
use std::thread;
let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely
Fix:
use std::sync::Arc;
use std::thread;
let arc = Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});
5. Practice Exercises
Exercise 1: Resilient Microservice Network Retry Engine with Exponential Backoff
Scenario:
In distributed microservice architectures, downstream remote calls frequently suffer from transient network outages or temporary rate-limiting. A common production pattern is an infinite loop expression acting as a stateful retry engine that evaluates to a Result<Response, NetworkError> using break statements.
Task:
Implement execute_with_retry<F>(config: RetryConfig, mut request_fn: F) -> Result<Response, NetworkError> where F: FnMut(u32) -> ServiceStatus.
- The function must execute an infinite
loopthat increments attempt counters and invokesrequest_fn(attempt). - If
ServiceStatus::Success(resp)is returned, exit the loop usingbreak Ok(resp). - If
ServiceStatus::TransientFailure(reason)is returned:- If
attempt >= config.max_attempts, exit the loop usingbreak Err(NetworkError::MaxRetriesExceeded { attempts: attempt, last_error: reason }). - Otherwise, update backoff state (
(current_backoff * 2).min(config.max_backoff_ms)) and invokecontinueto advance to the next retry attempt.
- If
- If
ServiceStatus::FatalFailure(reason)is returned, immediately exit usingbreak Err(NetworkError::Fatal(reason)).
Answer
Implementation
#[derive(Debug, PartialEq, Eq)]
pub enum NetworkError {
MaxRetriesExceeded { attempts: u32, last_error: String },
Fatal(String),
}
#[derive(Debug, PartialEq, Eq)]
pub struct Response {
pub status_code: u16,
pub body: String,
}
pub enum ServiceStatus {
Success(Response),
TransientFailure(String),
FatalFailure(String),
}
pub struct RetryConfig {
pub max_attempts: u32,
pub initial_backoff_ms: u64,
pub max_backoff_ms: u64,
}
pub fn execute_with_retry<F>(config: RetryConfig, mut request_fn: F) -> Result<Response, NetworkError>
where
F: FnMut(u32) -> ServiceStatus,
{
let mut attempt = 0;
let mut current_backoff = config.initial_backoff_ms;
let result = loop {
attempt += 1;
match request_fn(attempt) {
ServiceStatus::Success(resp) => {
break Ok(resp);
}
ServiceStatus::TransientFailure(reason) => {
if attempt >= config.max_attempts {
break Err(NetworkError::MaxRetriesExceeded {
attempts: attempt,
last_error: reason,
});
}
current_backoff = (current_backoff * 2).min(config.max_backoff_ms);
continue;
}
ServiceStatus::FatalFailure(reason) => {
break Err(NetworkError::Fatal(reason));
}
}
};
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_retry_success_first_attempt() {
let config = RetryConfig {
max_attempts: 3,
initial_backoff_ms: 100,
max_backoff_ms: 1000,
};
let result = execute_with_retry(config, |_attempt| {
ServiceStatus::Success(Response {
status_code: 200,
body: "OK".to_string(),
})
});
assert!(result.is_ok());
let resp = result.unwrap();
assert_eq!(resp.status_code, 200);
assert_eq!(resp.body, "OK");
assert_ne!(resp.status_code, 500);
assert!(matches!(resp, Response { status_code: 200, .. }));
}
#[test]
fn test_retry_transient_then_success() {
let config = RetryConfig {
max_attempts: 4,
initial_backoff_ms: 50,
max_backoff_ms: 400,
};
let result = execute_with_retry(config, |attempt| {
if attempt < 3 {
ServiceStatus::TransientFailure(format!("Timeout on attempt {}", attempt))
} else {
ServiceStatus::Success(Response {
status_code: 200,
body: "Recovered".to_string(),
})
}
});
assert!(result.is_ok());
let resp = result.unwrap();
assert_eq!(resp.body, "Recovered");
assert_ne!(resp.body, "Failed");
assert!(matches!(resp.status_code, 200));
}
#[test]
fn test_retry_max_retries_exceeded() {
let config = RetryConfig {
max_attempts: 3,
initial_backoff_ms: 10,
max_backoff_ms: 50,
};
let result = execute_with_retry(config, |attempt| {
ServiceStatus::TransientFailure(format!("503 Service Unavailable #{}", attempt))
});
assert!(result.is_err());
let err = result.unwrap_err();
assert_ne!(err, NetworkError::Fatal("503".to_string()));
assert!(matches!(
err,
NetworkError::MaxRetriesExceeded { attempts: 3, .. }
));
if let NetworkError::MaxRetriesExceeded { attempts, last_error } = err {
assert_eq!(attempts, 3);
assert!(last_error.contains("503 Service Unavailable #3"));
}
}
#[test]
fn test_retry_fatal_failure_immediate_break() {
let config = RetryConfig {
max_attempts: 5,
initial_backoff_ms: 10,
max_backoff_ms: 50,
};
let mut calls = 0;
let result = execute_with_retry(config, |_attempt| {
calls += 1;
ServiceStatus::FatalFailure("401 Unauthorized".to_string())
});
assert_eq!(calls, 1);
assert_ne!(calls, 5);
assert!(result.is_err());
assert!(matches!(result, Err(NetworkError::Fatal(_))));
}
}
Technical Explanation
loopas an Expression: In Rust,loopblocks are primary expressions that evaluate to a concrete value supplied bybreak expression;. The variableresultdirectly receives theResult<Response, NetworkError>evaluated by theloopblock without requiring intermediate mutable optional containers (Option<Result<...>>).- Type Uniformity & Language Invariants: Every
breakstatement inside a typedloopexpression must return the identical type. Inexecute_with_retry, all three exit arms (break Ok(resp),break Err(NetworkError::MaxRetriesExceeded { .. }), andbreak Err(NetworkError::Fatal(..))) evaluate toResult<Response, NetworkError>. If anybreakarm returned a mismatched type or omitted a value, the compiler would triggerE0308. - Ownership and State Mutation: The retry state variables (
attemptandcurrent_backoff) are mutated across iterations in the caller frame. The closurerequest_fnis declared withFnMut(u32)to allow mutable environment capture across retry iterations. - Edge Cases & Backoff Bounds: Exponential backoff calculations can overflow integer limits if uncontrolled;
.min(config.max_backoff_ms)ensures upper bounds are safe. WhenFatalFailureoccurs, the engine breaks immediately, guaranteeing zero wasted retry cycles or side effects on unrecoverable errors.
Exercise 2: Labeled Multi-Pass Stream Packet Framing & Validation Parser
Scenario:
High-performance binary network protocols transmit framed packets over contiguous stream buffers. Stream parsing requires resynchronizing corrupted headers and skipping malformed frames using nested loops with loop labels ('stream: loop and 'frame: loop).
Task:
Implement parse_stream_frames(buffer: &[u8]) -> ParseSummary.
- Frame protocol definition:
- Header: Magic byte
0xAA(1 byte). - Length: Payload length
L(1 byteu8). - Payload:
Lbytes. - Checksum: Bitwise XOR sum of payload bytes (1 byte).
- Header: Magic byte
- Control flow rules:
- Label the outer loop
'stream: loop. If remaining buffer bytes< 2, break out of'stream. - If
remaining[0] != 0xAA, resynchronize by incrementing cursor by 1 byte and callingcontinue 'stream. - If remaining buffer length is less than the complete frame size (
2 + L + 1), break out of'stream(buffer truncated). - Label the inner validation loop
'frame: loop. Calculate payload checksum. - If checksum matches, record
Frame { payload: payload.to_vec() }, advance cursor past the frame, and break out of'frame. - If checksum fails, increment corrupted frame counter, advance cursor by 1 byte past corrupt magic header, and call
continue 'streamto resume stream scanning.
- Label the outer loop
Answer
Implementation
#[derive(Debug, PartialEq, Eq)]
pub struct Frame {
pub payload: Vec<u8>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct ParseSummary {
pub frames: Vec<Frame>,
pub bytes_processed: usize,
pub corrupted_frames_skipped: usize,
}
pub fn parse_stream_frames(buffer: &[u8]) -> ParseSummary {
let mut frames = Vec::new();
let mut cursor = 0;
let mut corrupt_count = 0;
'stream: loop {
let remaining = &buffer[cursor..];
if remaining.len() < 2 {
break 'stream;
}
if remaining[0] != 0xAA {
cursor += 1;
continue 'stream;
}
let payload_len = remaining[1] as usize;
let total_frame_len = 2 + payload_len + 1;
if remaining.len() < total_frame_len {
break 'stream;
}
'frame: loop {
let payload = &remaining[2..2 + payload_len];
let expected_checksum = remaining[2 + payload_len];
let calculated_checksum = payload.iter().fold(0u8, |acc, &b| acc ^ b);
if calculated_checksum != expected_checksum {
corrupt_count += 1;
cursor += 1;
continue 'stream;
}
frames.push(Frame {
payload: payload.to_vec(),
});
cursor += total_frame_len;
break 'frame;
}
}
ParseSummary {
frames,
bytes_processed: cursor,
corrupted_frames_skipped: corrupt_count,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid_single_frame() {
let data = vec![0xAA, 0x03, 10, 20, 30, 10 ^ 20 ^ 30];
let summary = parse_stream_frames(&data);
assert!(summary.frames.len() == 1);
assert_eq!(summary.frames[0].payload, vec![10, 20, 30]);
assert_eq!(summary.bytes_processed, 6);
assert_ne!(summary.bytes_processed, 0);
assert!(matches!(
summary,
ParseSummary {
corrupted_frames_skipped: 0,
..
}
));
}
#[test]
fn test_parse_multiple_frames_with_noise() {
let data = vec![
0xFF, 0x00,
0xAA, 0x01, 42, 42,
0xBB,
0xAA, 0x02, 1, 2, 1 ^ 2,
];
let summary = parse_stream_frames(&data);
assert_eq!(summary.frames.len(), 2);
assert_eq!(summary.frames[0].payload, vec![42]);
assert_eq!(summary.frames[1].payload, vec![1, 2]);
assert_ne!(summary.corrupted_frames_skipped, 99);
assert!(matches!(summary.frames.as_slice(), [_, _]));
}
#[test]
fn test_parse_corrupt_checksum_recovery() {
let data = vec![
0xAA, 0x01, 42, 99,
0xAA, 0x01, 7, 7,
];
let summary = parse_stream_frames(&data);
assert_eq!(summary.frames.len(), 1);
assert_eq!(summary.frames[0].payload, vec![7]);
assert_eq!(summary.corrupted_frames_skipped, 1);
assert_ne!(summary.corrupted_frames_skipped, 0);
assert!(matches!(summary.frames.first(), Some(f) if f.payload == vec![7]));
}
#[test]
fn test_parse_truncated_buffer() {
let data = vec![0xAA, 0x05, 1, 2];
let summary = parse_stream_frames(&data);
assert!(summary.frames.is_empty());
assert_eq!(summary.bytes_processed, 0);
assert_ne!(summary.bytes_processed, 4);
assert!(matches!(summary.frames.len(), 0));
}
}
Technical Explanation
- Loop Labels & Scope Targeting: Rust allows loop constructs to be prefixed with explicit labels like
'stream:and'frame:. By invokingcontinue 'streamfrom inside the nested'frame: loop, control flow instantly unwinds the inner loop and resumes execution at the beginning of the outer stream iteration. - Zero-Copy Slicing & Lifetimes: During framing inspection, slices (
&remaining[2..2 + payload_len]) borrow directly from the input&[u8]buffer without heap allocation. Memory allocation forFramepayloadVec<u8>occurs exclusively after verifying payload integrity and checksum. - Stream Resynchronization & Safety: Binary protocol stream parsers must handle junk bytes or corrupted headers without panicking. When an invalid magic byte or corrupted checksum is encountered, the cursor advances by 1 byte and
continue 'streamre-scans the stream sequentially, preventing infinite loops on corrupted inputs. - Edge Cases: Buffer truncation (
remaining.len() < total_frame_len) breaks the outer loop gracefully, preserving already-parsed frames and returning the exact count of processed bytes.
Exercise 3: Lock-Free Ring Buffer Event Collector with CAS Retry Loop
Scenario:
In concurrent multi-threaded telemetry and logging pipelines, lock contention on traditional mutexes degrades throughput. Low-latency systems employ lock-free circular buffers where producer threads reserve write slots using atomic Compare-And-Swap (CAS) inside an infinite retry loop.
Task:
Implement LockFreeRingBuffer slot reservation using AtomicUsize and CAS retry loop:
reserve_slot(&self) -> Result<usize, BufferError>:- Enters a labeled atomic loop
'cas: loop. - Atomically reads current
headandtailpointers. - Checks buffer capacity: if
head.wrapping_sub(tail) >= self.capacity, breaks out of'casreturningErr(BufferError::Full). - Computes
next_head = current_head.wrapping_add(1). - Executes
self.head.compare_exchange_weak(current_head, next_head, Ordering::AcqRel, Ordering::Acquire). - On
Ok(_), breaks out of'casreturningOk(current_head % self.capacity). - On
Err(_), another thread updatedheadconcurrently; callscontinue 'casto retry the reservation with updated atomic values.
- Enters a labeled atomic loop
Answer
Implementation
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
#[derive(Debug, PartialEq, Eq)]
pub enum BufferError {
Full,
Shutdown,
}
pub struct LockFreeRingBuffer {
capacity: usize,
head: AtomicUsize,
tail: AtomicUsize,
}
impl LockFreeRingBuffer {
pub fn new(capacity: usize) -> Self {
Self {
capacity,
head: AtomicUsize::new(0),
tail: AtomicUsize::new(0),
}
}
pub fn reserve_slot(&self) -> Result<usize, BufferError> {
let result = 'cas: loop {
let current_head = self.head.load(Ordering::Relaxed);
let current_tail = self.tail.load(Ordering::Acquire);
if current_head.wrapping_sub(current_tail) >= self.capacity {
break 'cas Err(BufferError::Full);
}
let next_head = current_head.wrapping_add(1);
match self.head.compare_exchange_weak(
current_head,
next_head,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => {
let slot_idx = current_head % self.capacity;
break 'cas Ok(slot_idx);
}
Err(_) => {
continue 'cas;
}
}
};
result
}
pub fn advance_tail(&self, count: usize) {
self.tail.fetch_add(count, Ordering::Release);
}
pub fn len(&self) -> usize {
let head = self.head.load(Ordering::Relaxed);
let tail = self.tail.load(Ordering::Relaxed);
head.wrapping_sub(tail)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_thread_slot_reservation() {
let buffer = LockFreeRingBuffer::new(4);
assert_eq!(buffer.reserve_slot(), Ok(0));
assert_eq!(buffer.reserve_slot(), Ok(1));
assert_eq!(buffer.reserve_slot(), Ok(2));
assert_eq!(buffer.reserve_slot(), Ok(3));
let err = buffer.reserve_slot();
assert!(err.is_err());
assert_eq!(err, Err(BufferError::Full));
assert_ne!(err, Ok(4));
assert!(matches!(err, Err(BufferError::Full)));
}
#[test]
fn test_tail_advance_frees_capacity() {
let buffer = LockFreeRingBuffer::new(2);
assert!(buffer.reserve_slot().is_ok());
assert!(buffer.reserve_slot().is_ok());
assert_eq!(buffer.reserve_slot(), Err(BufferError::Full));
buffer.advance_tail(1);
let res = buffer.reserve_slot();
assert!(res.is_ok());
assert_eq!(res, Ok(0));
assert_ne!(res, Ok(1));
assert!(matches!(res, Ok(0)));
}
#[test]
fn test_concurrent_multi_thread_reservations() {
let capacity = 1000;
let buffer = Arc::new(LockFreeRingBuffer::new(capacity));
let num_threads = 10;
let slots_per_thread = 100;
let mut handles = Vec::new();
for _ in 0..num_threads {
let buf_clone = Arc::clone(&buffer);
let handle = thread::spawn(move || {
let mut local_slots = Vec::new();
for _ in 0..slots_per_thread {
if let Ok(slot) = buf_clone.reserve_slot() {
local_slots.push(slot);
}
}
local_slots
});
handles.push(handle);
}
let mut total_reserved = 0;
for handle in handles {
let thread_slots = handle.join().unwrap();
total_reserved += thread_slots.len();
}
assert_eq!(total_reserved, 1000);
assert_eq!(buffer.len(), 1000);
assert_ne!(buffer.len(), 0);
assert!(matches!(buffer.reserve_slot(), Err(BufferError::Full)));
}
}
Technical Explanation
- Lock-Free CAS Loops: In concurrent Rust,
compare_exchange_weakinside an explicitloopprovides non-blocking synchronization. If thread A loses a race to thread B,compare_exchange_weakreturnsErr(actual_value), causing the loop to callcontinue 'casand retry with the updated state rather than acquiring a blocking kernel mutex lock. - Memory Ordering & Hardware Barriers:
Ordering::AcqRelon success ensures write operations performed prior to slot reservation become visible to consumer threads reading slot data (Release), while acquiring prior consumer tail updates (Acquire). - Wrapping Arithmetic & Overflow: Atomic counters wrapping around integer boundaries (
usize::MAX) are safely calculated usingwrapping_subandwrapping_add. The distancehead.wrapping_sub(tail)correctly yields the number of active items regardless of counter rollover. - Thread Safety &
SyncTrait:LockFreeRingBufferimplementsSyncautomatically because all inner fields (AtomicUsize,usize) implementSync, permitting safe shared borrowing across thread handles wrapped inArc.
6. Related Terms
while— A loop that runs as long as a specific condition evaluates to true.for/ Range — An iterator loop (the most common and idiomatic loop in Rust).- Expressions — The concept that allows
loopto return a value.
7. Key Takeaways
loopcreates an infinite loop.- Use the
breakkeyword to exit the loop entirely. - Use the
continuekeyword to skip the rest of the current iteration and start the next one immediately. loopcan be used as an expression to return a value by passing that value tobreak(e.g.,break 42;).- Always prefer
loopoverwhile true.