skip to content

In PHP, why does a second foreach over the same Generator object throw, and how do you iterate the sequence twice?

level: middleimportance: should knowfreq 40%

answer

  1. foreach calls rewind() first
  2. forward-only, no replay of side effects
  3. Cannot traverse an already closed generator
  4. Cannot rewind a generator that was already run
  5. call the generator function again

basics

~20 s

A Generator is forward-only: foreach calls rewind(), which is allowed only before the generator passes its first yield, and a finished generator cannot be traversed at all. Iterate twice by calling the generator function again.

solid answer

~40 s

`foreach` starts by calling `rewind()`. A `Generator` can honour that only while it sits at its first `yield`; once it has advanced further, `rewind()` throws `Exception` with *Cannot rewind a generator that was already run*, and a generator that already finished is rejected earlier with *Cannot traverse an already closed generator*. The design is deliberate: the body may have read a stream or advanced a cursor, and replaying it would mean re-running those side effects. A second pass therefore needs a **new** generator, made by calling the generator function again, often through a small factory closure or a class whose `getIterator()` returns a fresh one. `clone` is not an escape hatch: generators are uncloneable and throw `Error`. If the data is small, materialising it into an array once is also fine.

code

php · 18 lines
php
<?php
function numbers(): Generator
{
    yield 1;
    yield 2;
    yield 3;
}

$gen = numbers();
foreach ($gen as $n) { echo $n; }   // 123

try {
    foreach ($gen as $n) { echo $n; }
} catch (Exception $e) {
    echo $e->getMessage();          // Cannot traverse an already closed generator
}

foreach (numbers() as $n) { echo $n; } // 123: a fresh generator

go deeper

for a junior

Recall that a generator is iterated once, forward only, and that looping again needs a new call to the generator function.

for a middle

Explain that foreach calls rewind(), which a generator allows only at its first yield, and name the two exceptions for an advanced and a finished generator.

for a senior

Spot the review bug where a counting or logging pass consumes a generator before the real loop, and choose between re-calling, a factory closure or materialising.

for a principal

Decide whether an API should hand callers a single-pass generator or a re-iterable object, weighing memory against callers who reasonably expect to loop twice.

## What foreach does to a Generator When `foreach` receives an object that implements `Iterator`, it drives it through the interface methods: `rewind()` once, then `valid()`, `current()`, `key()` and `next()` for each element. A `Generator` implements `Iterator`, so the same steps apply. The difference is in what `rewind()` can do. For a `Generator`, `rewind()` means *run the body up to the first yield, if it has not got there yet*. It cannot mean *go back to the start*, because a generator has no stored copy of the values it produced; it only has its paused execution state. ## The two errors There are two distinct failure points, and both raise a plain `Exception` (not an `Error`): | State of the generator | What a new foreach does | |---|---| | created, never pulled | runs the body to the first yield and iterates normally | | paused at its first yield (for example the previous loop did `break` on the first element) | `rewind()` is a no-op; the loop starts again from that first value | | advanced past its first yield | `rewind()` throws *Cannot rewind a generator that was already run* | | finished (body returned) | `foreach` throws *Cannot traverse an already closed generator* before calling `rewind()` | The first-yield row in the table is worth knowing: a loop that broke out right after the first value leaves the generator exactly where a fresh `rewind()` would put it, so the next loop sees that value again. One more element consumed and the same code throws. ## Why PHP refuses to replay The restriction follows from what generators are for: - **Side effects.** A generator that reads a file with `fgets()`, consumes a queue, or pages through an API has changed the world by the time it yields. Replaying the body would read more lines, take more messages or send more requests, not reproduce the old values. - **No buffer.** Holding every produced value so that it could be replayed would bring back the memory cost the generator avoided. - **Honest semantics.** Throwing early is safer than silently yielding nothing on the second pass, which is what a naive implementation would do. The PHP manual states this directly: generators are forward-only iterators, and the same generator cannot be iterated more than once; it has to be rebuilt by calling the generator function again. ## How to iterate twice Pick the approach by what the second pass needs: 1. **Call the generator function again.** `lines($path)` a second time opens the file again and streams it again. This is the normal answer and keeps memory flat. 2. **Pass a factory, not a generator.** When a function needs to loop more than once over a source it receives, accept a `Closure` that returns a fresh generator, such as `fn () => lines($path)`, and call it per pass. 3. **Return a fresh generator per iteration from a class.** A collection class can implement `getIterator()` as a generator method, so every `foreach` over the object calls it and gets a new `Generator`. 4. **Materialise once.** If the sequence is small and expensive to produce, collect it into an array and loop over the array as often as needed. This trades memory for not repeating the work. What does **not** work: - `clone $generator` throws `Error` (*Trying to clone an uncloneable object of class Generator*). - Calling `$gen->rewind()` yourself after the first loop throws the same *already run* exception that `foreach` hits. - Serialising a generator is not allowed either, so it cannot be stored and restored. ## Spotting the bug in review The defect usually hides behind a variable name. A function computes `$rows = $repo->streamRows();`, logs `iterator_count($rows)` for metrics, then loops `foreach ($rows as $row)` to process them. The count consumed the generator; the loop then throws *Cannot traverse an already closed generator*. The fix is either to count while processing in the single loop, or to call `streamRows()` twice if two passes are genuinely needed.

  • A service logs iterator_count($rows) and then loops over $rows, where $rows came from a generator. What fails, and how do you fix it?
    `iterator_count()` runs the generator to the end, so the following `foreach` throws *Cannot traverse an already closed generator*. Count inside the single processing loop instead, or, if two passes are genuinely needed, call the generator function twice so each pass gets its own `Generator`.
  • Why does a second foreach work after the first one broke out on its very first value?
    Breaking on the first value leaves the generator paused at its first `yield`, which is exactly where `rewind()` would put it. `Generator::rewind()` is a no-op in that state, so the second loop starts from the same first value. Had the first loop consumed one more value, the second would throw.

A generator is like a live radio broadcast rather than a recording: you hear each song as it plays, and once the broadcast has moved on there is no rewind button; to hear the set again you need a new broadcast.

saying these in an interview costs you the question

  • A second foreach over a finished generator simply yields nothing.
  • Calling rewind() on a generator restarts its body from the top.
  • Cloning a generator gives you an independent copy to iterate again.
  • Generators replay their values from an internal buffer on each loop.
  • The rewind failure is an Error subclass, so catch (Exception) misses it.