1. Prompt and scope
A log aggregator reads an in-memory array, a deduplicated Set, and a paginated generator in priority order; the consumer needs only the first 100 records. Use Iterator.concat while preserving laziness, and explain infinite inputs, exceptions, and cleanup when consumption stops early.
2. What the interviewer is testing
- Distinguish Iterable from Iterator and know that
Iterator.concatrequires Iterable arguments. - Explain that the result is a new lazy Iterator that pulls inputs in sequence only when consumed.
- Identify that an infinite source hides every later source and that the argument list must remain finite.
- Handle
returncleanup, TypeError cases, compatibility fallback, and unnecessary array materialization.
3. Questions to clarify first
- Is each input a repeatable Iterable or an already-advanced single-use Iterator?
- Does the consumer need a streaming interface or an array at the boundary?
- Do any inputs own a file, network, or database cursor that must close on early stop?
- Do target browsers and Node provide native
Iterator.concat, or is a polyfill allowed?
4. Thirty-second answer
I would verify that every argument implements Symbol.iterator, then create a lazy Iterator with Iterator.concat. It acquires each input in order and reads values only when the consumer calls next, take, or spreads the result. take(100) stops pulling and gives the underlying iterator a chance to close. Put infinite inputs last or bound consumption; on older runtimes, use a generator fallback with the same laziness and cleanup semantics.
5. Step-by-step deep dive
Step 1: Establish the Iterable boundary
Iterator.concat accepts Iterables, not merely objects with next. Arrays, Sets, and generator objects qualify. A bare Iterator should first be wrapped with Iterator.from; this gives concat a clear way to obtain and close each input iterator.
Step 2: Build the lazy composition
function* pages() {
yield { source: "page", id: 1 };
yield { source: "page", id: 2 };
}
const memory = [{ source: "memory", id: 1 }];
const unique = new Set([{ source: "set", id: 1 }, { source: "set", id: 2 }]);
const merged = Iterator.concat(memory, unique, pages());
const firstThree = merged.take(3).toArray();Creating merged does not traverse any input; toArray starts consumption. Values are emitted in memory, Set, and pages order, and concat does not deduplicate or transform them.
Step 3: Understand consumption state
The returned Iterator stores the current input and position. After one consumer advances it, another consumer sees only the remainder. Replaying requires fresh Iterators from the source Iterables. If only the first N values are needed, apply take after concat instead of spreading first.
Step 4: Bound infinite inputs
Any input may be infinite, making the result infinite. If the first input never ends, later inputs are unreachable; place finite batches first or enforce take, timeout, and cancellation at the consumer boundary. Do not write Iterator.concat(...infiniteIterables()), because spreading the argument list never finishes.
Step 5: Cleanup, errors, and fallback
When consumption stops or throws, the current iterator should get an opportunity to run return, releasing cursors, file handles, or connections. A custom iterator can record cleanup in finally. On runtimes without native support, a generator can yield* inputs in order and propagate return; the fallback must not silently copy every input into an array.
6. Model high-quality answer
I would validate Iterable inputs, create a lazy composition withIterator.concat, and calltake(100)ortoArrayonly at the consumer boundary. concat acquires inputs in order and does not deduplicate their values. An infinite first input makes later sources unreachable, so I would bound consumption and test early-stop cleanup throughreturn. For older runtimes, a generator fallback can preserve laziness, ordering, single-use state, and cleanup without materializing the sources.
7. Common mistakes
- Passing an object with only
next→ TypeError → wrap it withIterator.fromor implementSymbol.iterator. - Assuming concat executes immediately → hidden I/O → remember that consumption pulls values.
- Putting an infinite Iterable first → later sources are never reached → reorder and bound consumption.
- Spreading everything first → memory and latency spikes → call
toArrayonly at an explicit array boundary. - Ignoring early-stop
return→ leaked cursors → test a counting Iterable and assert cleanup.
8. Follow-up questions
Follow-up 1: How does it differ from Array.concat?
Array concat processes materialized arrays immediately and returns an array. Iterator.concat accepts Iterables and returns a lazy Iterator that can represent generators and infinite sources.
Follow-up 2: Why not pass every Iterator directly?
A bare Iterator that is not Iterable leaves ownership and closure of not-yet-reached inputs ambiguous. Wrap it with Iterator.from to establish the Iterable boundary.
Follow-up 3: When is flatMap preferable?
When the input itself is a large or unbounded sequence of Iterables, flatMap can produce and flatten them incrementally. concat is clearer for a finite, known set of inputs.
Follow-up 4: How do you verify cleanup?
Use a test Iterable implementing next and return, record pulls and closes, and assert that take, consumer interruption, and callback errors stop pulling and call return.