Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-230 · Case file with fixtures · Case 202 of 694 · Runtime evidence

Iterator::nth Consumes the Skipped Items and the Selected Item

Iterator::nth advances through n skipped items and consumes the item it returns. Use indexing or a cloned iterator for observation without advancing, and make the cursor transition visible in tests.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Iterator::nth advances through every skipped item and consumes the selected item it returns, leaving the iterator positioned after that answer.
First discriminating check
Call next immediately after nth on a four-item iterator and compare observation through a slice with advancement through the iterator.

I used nth(2) as if it were indexing an iterator. It returned the value I wanted, but the iterator had moved farther than the surrounding code expected. The next stage never saw the earlier values or the selected value again.

The failing program calls nth(2) on 10, 20, 30, 40. It receives 30. A following next receives 40, not 20.

nth is an advancing operation

Iterator::nth returns the nth element of the iterator, where the first item is position zero. To reach it, the method consumes the preceding n items. The returned item is consumed too.

The state transition is easier to see as calls to next:

nth(2) = discard next() -> 10
         discard next() -> 20
         return  next() -> 30
remaining iterator      -> 40

Calling nth(0) repeatedly is equivalent to calling next repeatedly. Calling nth(1) repeatedly skips every other remaining item. The documentation calls out both behaviours because the method name can look like random access.

An iterator does not promise random access

Some iterator sources sit over arrays, but others decode a stream, walk a tree, receive messages, or generate values. A general iterator cannot jump to an item without advancing its source.

Even when the source supports fast skipping, the semantic result is still consumption. An implementation can override nth for efficiency, but it cannot preserve skipped items for later iteration and still follow the same contract.

This matters when skipped items own resources. Their destructors run as they are discarded. An iterator over results can also hide errors if code skips past them without inspecting each value.

Observation needs another primitive

The repaired program uses a slice iterator's as_slice method and indexes the remaining slice. This observes the requested item without changing iterator state.

That repair is deliberately specific to a slice iterator. For a cloneable iterator, I can clone its current state and call nth on the clone. I do this only when cloning represents an independent cursor and does not duplicate external effects.

For a single-pass stream, non-consuming lookahead needs buffering. Peekable gives one-item lookahead; looking several items ahead requires an explicit queue or parser buffer. The storage cost is part of the design rather than something nth provides for free.

skip(n).next() has the same consumption model

Rewriting as iterator.skip(2).next() does not preserve anything. skip owns and advances the iterator, and next consumes the selected item. It may read more clearly in a pipeline, but it is not an indexing repair.

advance_by makes pure advancement explicit and reports how far an exhausted iterator fell short. I use it when discarding a prefix is the actual goal.

The choice becomes simple once I name the action: observe, peek, buffer, or advance.

Partial exhaustion also changes the source

If the iterator contains only two values, nth(5) returns None after consuming both. The failure to reach position five does not roll the iterator back.

This is important in parsers that try one interpretation and then attempt another. Using nth during speculative parsing commits consumption. A transactional parser needs an index into stable input, a checkpoint that can be restored, or a buffered cursor designed for backtracking.

My regression asserts state after the answer

An assertion that nth(2) == Some(30) proves only the returned value. It cannot reveal the mistaken state assumption. The fixture immediately calls next and asserts the remaining frontier.

I also test nth(0), an index exactly at the end, an index beyond the end, and an iterator with visible drop probes when discarded-item lifecycle matters. For parser code, the regression checks the source position and error span after an unsuccessful lookahead.

This makes later refactoring of the iterator source observable too.

The wider principle is that queries on stateful cursors can also be transitions. nth answers by advancing. When I need observation, I use a data structure that supports observation; when I accept consumption, I make the new frontier part of the contract and the test.