Chapter 3

Conditionals and Loops

👋 Anyone can read and edit this exercise. Sign up to save your progress.

Why did the Rust loop break up with the condition? It said "I just need some space."

You've already seen if and for in passing. This chapter slows down and looks at them on purpose, plus the other two loop forms (while and loop) and the keywords that control them (break and continue).

if / else / else if

The usage is unsurprising:

if x > 0 {
    println!("positive");
} else if x < 0 {
    println!("negative");
} else {
    println!("zero");
}

Two things to call out:

for loops

for walks anything that produces an iterator. Here's how it works:

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

for word in ["hi", "rust"] {
    println!("{word}");
}

0..5 is a range: a value that produces the integers from 0 up to (but not including) 5. The inclusive form is 0..=5, which also yields 5. Both work as iterators and as patterns in match (seen in the password chapter later).

For larger collections, you'll usually iterate over a Vec, a slice, a HashMap, or the result of s.chars(). Iterators get their own chapter (chapter 16); for now, "anything you can put on the right of for x in ..." is enough.

while and loop

while runs as long as a condition is true:

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

loop runs forever, until you break out of it. Useful when the exit condition isn't a simple boolean check at the top:

let mut attempts = 0;
loop {
    attempts += 1;
    if try_connect() { break; }
    if attempts > 10 { break; }
}

loop can also produce a value: pass an expression to break and the whole loop evaluates to it.

let answer = loop {
    let guess = read_guess();
    if guess == 42 { break guess; }
};

break and continue

Both keywords control the innermost loop:

for n in 0..10 {
    if n % 2 == 1 { continue; }   // skip odd
    if n > 6     { break; }       // stop at 8
    println!("{n}");              // 0, 2, 4, 6
}

Picking the right loop

A useful rule of thumb:

Most code reaches for for. Iterators (covered later in the course) make for even more powerful.

Ferris's mood

Ferris the crab is a creature of simple needs. Two things determine his mood on any given day: how hungry he is (on a 0..=10 scale) and how many naps he's managed to fit in.

Implement ferris_mood(hunger, naps) returning a &'static str, following these rules:

ConditionMood
hunger >= 8"Hangry"
hunger >= 5 and naps == 0"Grumpy"
naps >= 3"Sleepy"
anything else"Content"

&'static str just means "a borrowed string slice that lives for the whole program". String literals like "Hangry" are baked into your compiled binary, so the text is around for as long as the program is running. The 'static lifetime is just the compiler's way of saying "this reference will never dangle." If you've written C, it's the same intuition as a const char * pointing at a string literal. Lifetimes get a proper introduction in chapter 12 alongside ownership and borrowing; for now the only thing to take away is "string literals are always safe to return as &'static str."

Two things to watch

Combining conditions. The "Grumpy" rule needs both parts to be true. Rust spells this && (logical AND). Its sibling || is logical OR. Both short-circuit: if the left side already decides the answer, the right side isn't evaluated.

Order matters. An if/else if/else chain is checked top-to-bottom and stops at the first match. If you put the naps check before the hunger check, a hungry crab who happens to have napped a lot will get classified as "Sleepy" instead of "Hangry". The tests deliberately include cases (like ferris_mood(9, 5)) that only pass with the right ordering.

Exercise 1 of 4
Open in Web Editor

Results

    Compiler / runtime output
    
                
    Stuck? Show a hint No spoilers, just a nudge
    1. The order of an if/else if chain decides everything: top to bottom, first match wins. Translate the rule table line by line and the order falls out for you.
    2. The "Grumpy" rule needs both conditions to be true. Combine them with && (logical AND).
    3. fn ferris_mood(hunger: u32, naps: u32) -> &'static str {
          if hunger >= 8 {
              "Hangry"
          } else if hunger >= 5 && naps == 0 {
              "Grumpy"
          } else if naps >= 3 {
              "Sleepy"
          } else {
              "Content"
          }
      }
      

    Factorial with a `for` loop

    n! is 1 * 2 * 3 * ... * n. By convention, 0! == 1. Build it up with a running accumulator and a for loop over an inclusive range.

    The accumulator pattern shows up everywhere once you start writing loops: let mut acc = ...; for x in ... { acc = ... }; acc. Note the mut: bindings are immutable by default, and the loop body needs to update acc, so you have to opt in.

    Exercise 2 of 4
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  
      Stuck? Show a hint No spoilers, just a nudge
      1. let mut acc: u32 = 1; outside the loop, for i in 1..=n { ... } inside. Return acc at the end.
      2. The body of the loop is acc *= i;. Both mut on the binding and *= for the compound assignment are needed.
      3. fn factorial(n: u32) -> u32 {
            let mut acc: u32 = 1;
            for i in 1..=n {
                acc *= i;
            }
            acc
        }
        

      Counting evens with `for` and `continue`

      Now you have a slice of integers and want to know how many of them are even. The natural approach is a for loop over the slice with a counter that you bump on every match.

      This is a good place to use continue: skip the odds early and the "do work" branch ends up uncluttered.

      Exercise 3 of 4
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    
        Stuck? Show a hint No spoilers, just a nudge
        1. let mut count = 0u32; plus a for n in numbers loop. The suffix 0u32 pins the integer type so you don't need a separate annotation.
        2. for n in numbers over a &[i32] yields &i32. The % operator works through the reference, so n % 2 Just Works. continue skips the rest of the current iteration.
        3. fn count_evens(numbers: &[i32]) -> u32 {
              let mut count = 0u32;
              for n in numbers {
                  if n % 2 != 0 { continue; }
                  count += 1;
              }
              count
          }
          

        Counting digits with `while`

        How many digits does a number have? 0 has one digit; everything else is "divide by 10 and count how many times you can do it before hitting zero". That's a natural while loop: keep going as long as the number is non-zero, dividing it down each step.

        This is the inverse of a for loop (like the one you wrote for factorial). With factorial, you knew up front how many times to loop. Here, you don't: you have to keep dividing until the number runs out. That's exactly what while is for.

        Exercise 4 of 4
        Open in Web Editor

        Results

          Compiler / runtime output
          
                      
          Stuck? Show a hint No spoilers, just a nudge
          1. Special-case n == 0 returning 1. Otherwise, divide by 10 in a while loop and count the iterations.
          2. Shadow the parameter with let mut n = n; so you can mutate it without changing the signature. Loop while n > 0, dividing by 10 and bumping a counter.
          3. fn digit_count(n: u32) -> u32 {
                if n == 0 { return 1; }
                let mut n = n;
                let mut count = 0u32;
                while n > 0 {
                    n /= 10;
                    count += 1;
                }
                count
            }
            

          Wrapping up conditionals and loops

          You wrote a three-way classifier with if/else if/else, two accumulator-style loops (a for over a range and a for over a slice with continue), and a while loop where the iteration count isn't known up front.

          What we learned

          • if/else is an expression, not just a statement. It can sit on the right of let, be returned from a function, or appear anywhere a value is expected. Both branches must have the same type.
          • Conditions are bare bool expressions. No parentheses required, no implicit conversion from integers or strings.
          • for x in iter is the default loop. Ranges (0..n, 0..=n), slices, vectors, and most other collections all produce iterators you can put on the right.
          • while cond runs as long as the condition is true. Reach for it when the iteration count depends on values computed inside the loop (like "divide until zero").
          • loop runs forever until you break. It can also produce a value: let x = loop { ...; break value; };.
          • break exits the innermost loop; continue skips to the next iteration. A continue to early-out the boring case usually reads better than nesting the work inside an if.
          • The accumulator pattern (let mut acc = ...; for ... { acc = ...; }) is the how you can "compute one value from many". Once you meet iterators in chapter 15, methods like sum, count, and fold will replace many of these by-hand loops.
          Next chapter 4Functions