Mehdi Akiki
Rust Failure Atlas / Upgrades and compatibility

RFA-399 · Case file with fixtures · Case 371 of 694 · Compiler evidence

Iterator::advance_by Remains Unstable on Rust 1.98

Iterator::advance_by remains guarded by iter_advance_by on Rust 1.98.1. A stable explicit next loop can report how many requested steps remain when the iterator ends, while making consumption and side effects visible.

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

Direct answer

What this Rust failure means

Why it happens
The method is documented but still guarded by iter_advance_by, so stable code must express advancement and shortfall with existing Iterator::next operations.
First discriminating check
Read the stability marker for the exact toolchain and implement a small next loop when the remaining shortfall must be reported portably.

Skipping a number of iterator items sounds basic enough that I expected advance_by to be an ordinary stable method. On Rust 1.98.1, that expectation is wrong.

The failing fixture calls the documented method and compilation fails with E0658. The diagnostic names iter_advance_by, showing that the API remains behind an unstable feature gate.

There is a second trap waiting after stabilization: the error reports the remaining shortfall, not the original requested amount.

Documentation is not a stability promise

Iterator::advance_by describes advancing by n calls to next. A successful result is Ok(()). If the iterator ends early, the error contains a non-zero count of how many advances could not be completed.

That is a useful contract, but the Rust 1.98.1 page marks it experimental. Stable projects cannot enable it just by importing another trait. The E0658 documentation explains the channel restriction.

I verify availability against the minimum supported Rust version of the project, not only the newest toolchain installed on my machine. For libraries, that minimum is part of the user contract.

The error is a remainder

Suppose I request five advances from an iterator containing two items. Two calls succeed, the third sees the end, and three requested steps remain. The intended error value is therefore three.

It is not five, because some progress happened. It is not two, because the error does not report consumed items. It is not an index into the original sequence.

Naming this value remaining or shortfall prevents mistakes. A name such as count is too vague in retry and pagination code.

The stable repair is deliberately boring

The repaired fixture uses the oldest and clearest iterator operation:

fn advance_by_stable<I: Iterator>(
    values: &mut I,
    requested: usize,
) -> Result<(), usize> {
    for completed in 0..requested {
        if values.next().is_none() {
            return Err(requested - completed);
        }
    }
    Ok(())
}

If the first next fails, completed is zero and every requested step remains. If two items were consumed before failure, the loop reaches completed == 2, so requested - completed is the uncompleted remainder.

For requested == 0, the range is empty, no call is made, and the function succeeds. This boundary falls naturally out of the loop without subtracting one from zero.

Why I do not automatically replace it with nth

Iterator::nth can skip elements and return a later one, but its contract asks a different question. nth(n) consumes and returns the element at offset n; it therefore consumes up to n + 1 items.

To advance exactly n positions with nth, code is tempted to call nth(n - 1). That needs a special case for zero and returns the last consumed item rather than a shortfall count. The explicit loop is easier to audit when exact progress reporting matters.

An optimized custom iterator may eventually provide faster internal advancement through the standard method once it is stable. My fallback favors stable semantics and clarity, not a claim of maximum specialization.

Advancing drives lazy work

Skipped values are not necessarily free values sitting in memory. Calling next can parse input, read a file, receive from a generator, increment counters, or execute adapter closures.

Advancing means performing and discarding that iterator work. It does not seek an arbitrary data source unless the iterator implementation can optimize the operation under its contract.

This matters when code says “just skip one million rows.” For a streaming decoder, that can still mean decoding one million rows. If random access or seeking is required, the data source needs an API that promises it.

Early exhaustion permanently consumes progress

The error does not roll back successful steps. In the two-of-five example, the two existing items are gone and the iterator is exhausted. Retrying the remaining three on that same iterator immediately fails again with a shortfall of three.

For checkpointing, I record completed progress before deciding whether an early end is an error, normal completion, or a reason to fetch another segment. A Result return can make failure look transactional when it is actually a partial state transition.

The fixed fixture asserts the shortfall and then calls next to prove the source is exhausted. It also tests a successful two-step advance and verifies that the third item remains.

A wrapper gives upgrades one boundary

I keep the stable helper local to the behavior that needs it. Its error type is usize, while the intended standard method uses a non-zero integer wrapper. That difference is acceptable inside a private adapter with tests, but I would consider a stronger domain error before exposing it as a public API.

When a future minimum Rust version stabilizes advance_by, the wrapper is one place to adopt it. Until then, every build stays on the promised stable channel.

The review checklist

I ask four questions around iterator advancement:

  • Is the chosen method stable on the pinned minimum Rust version?
  • Does the returned number mean completed work or remaining work?
  • What side effects happen for each consumed item?
  • What cursor position remains after success or early exhaustion?

These questions catch more bugs than treating advancement as pointer arithmetic.

The core principle is that skipping is still consumption. Rust 1.98.1 does not yet expose advance_by to stable code, so an explicit next loop is a good compatibility boundary. It also makes partial progress and the meaning of the shortfall difficult to misunderstand.