#246 Jul 7, 2026

246. Iterator::peekable — Look at the Next Item Without Consuming It

Sometimes you need to see the next element to decide what to do — but calling .next() eats it. .peekable() gives you a .peek() that shows the next item while leaving it in place.

The problem: deciding based on what comes next

A classic case is joining items with a separator. You want a comma between elements but not a trailing one, so you need to know “is there another item after this?” A plain iterator can’t tell you without consuming it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn join_csv(items: &[&str]) -> String {
    let mut out = String::new();
    let mut it = items.iter().peekable();
    while let Some(item) = it.next() {
        out.push_str(item);
        if it.peek().is_some() {
            out.push_str(", ");
        }
    }
    out
}

assert_eq!(join_csv(&["a", "b", "c"]), "a, b, c");
assert_eq!(join_csv(&["solo"]), "solo");

peek() returns Option<&Item> — a reference to the next value if there is one — without advancing the iterator. The next .next() still hands you that same element.

The real power: peek to decide, then consume

Peeking shines when you’re parsing a stream and want to grab a run of elements that match a condition. Look at the front, and only call .next() once you’ve decided to keep it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::iter::Peekable;
use std::str::Chars;

// Read a run of digits off the front of a char stream.
fn take_number(chars: &mut Peekable<Chars>) -> u32 {
    let mut n = 0;
    while let Some(&c) = chars.peek() {
        match c.to_digit(10) {
            Some(d) => {
                n = n * 10 + d;
                chars.next(); // commit: actually consume it
            }
            None => break, // leave the non-digit in place
        }
    }
    n
}

let mut chars = "42px".chars().peekable();
assert_eq!(take_number(&mut chars), 42);
// The "px" is untouched, ready for the next parser.
assert_eq!(chars.collect::<String>(), "px");

The non-digit p stays in the iterator because we peeked at it instead of consuming it — the caller picks up exactly where the number ended.

next_if for the common case

When the pattern is “consume the next item only if it matches,” next_if does the peek-and-maybe-advance in one call:

1
2
3
4
let mut it = [1, 2, 3].iter().peekable();
assert_eq!(it.next_if(|&&x| x == 1), Some(&1)); // matches, consumed
assert_eq!(it.next_if(|&&x| x == 99), None);    // no match, 2 stays put
assert_eq!(it.next(), Some(&2));

There’s also next_if_eq for the “advance past this exact value” case. Whenever you find yourself wishing you could un-call .next(), reach for .peekable().

← Previous 245. rem_euclid — The Modulo That Never Goes Negative