TL;DR: Iterator.concat(a, b, c) walks any iterables one after another and returns a lazy iterator. No temporary array, no hand-written generator, and it has been Baseline Newly available since March 2026.
The array you build just to throw away
You have an array, a Set, and a generator. You want to loop over all of them in order. So you write the line everyone writes:
for (const item of [...pinned, ...recentSet, ...loadMore()]) {
render(item);
}
It works. It also copies every item into a brand new array, runs the generator to the very end, and then hands the array to the garbage collector a moment later. If you only needed the first few items, you paid for all of them.
The careful version is a generator with yield*:
function* concat(...iterables) {
for (const iterable of iterables) {
yield* iterable;
}
}
Correct, lazy, and copy-pasted into a utils folder near you.
What it is
Iterator.concat() is that generator, built into the language. It takes any number of iterables and returns an iterator that yields from each one in turn:
const array = [1, 2, 3];
const set = new Set([4, 5]);
function* gen() {
yield 6;
yield 7;
}
console.log([...Iterator.concat(array, set, gen())]);
// [1, 2, 3, 4, 5, 6, 7]
The result is a real iterator, so the iterator helpers work on it directly:
const evensTimesTen = Iterator.concat([1, 2, 3], new Set([4, 5, 6]))
.filter((n) => n % 2 === 0)
.map((n) => n * 10)
.toArray();
console.log(evensTimesTen); // [20, 40, 60]
Maps are iterables of [key, value] pairs, which makes "defaults, then overrides" a one-liner:
const defaults = new Map([["theme", "light"], ["lang", "en"]]);
const user = new Map([["theme", "dark"]]);
const settings = new Map(Iterator.concat(defaults, user));
console.log(Object.fromEntries(settings)); // { theme: "dark", lang: "en" }
Later keys win, because that is how the Map constructor already behaves.
The realistic one: take only what you need
Here is where laziness pays. Two sources of 1000 rows each, and a UI that shows the first three:
let pulled = 0;
function* page(start) {
for (let i = start; i < start + 1000; i++) {
pulled++;
yield i;
}
}
const spread = [...page(0), ...page(1000)].slice(0, 3);
console.log(spread, pulled); // [0, 1, 2] 2000
pulled = 0;
const lazy = Iterator.concat(page(0), page(1000)).take(3).toArray();
console.log(lazy, pulled); // [0, 1, 2] 3
Same answer. The spread version produced 2000 rows to keep 3. The concat version produced 3. Replace "row" with "parsed log line" or "decoded image" and that gap stops being academic.
It is also lazy about opening each source. Logging every [Symbol.iterator]() call on two iterables gave this order:
created
open a
next 1
next 2
open b
next 3
Nothing is opened when you call concat, and b is not touched until a runs out. That also means infinite sources are fine, as long as you stop early:
function* naturals() {
let i = 1;
while (true) yield i++;
}
console.log(Iterator.concat(["start"], naturals()).take(5).toArray());
// ["start", 1, 2, 3, 4]
Can I use it?
| Where | Status |
|---|---|
| Baseline | Newly available since March 2026 |
| Chrome and Edge | 146 |
| Firefox | 147 |
| Safari (macOS and iOS) | 26.4 |
| Node.js 24 | Not yet: typeof Iterator.concat is "undefined" in v24.11.0 |
| Spec | TC39 Iterator Sequencing proposal, Stage 4 |
Everything in this post ran in Chrome 156. If you need it in Node today, keep the five-line generator from above as a fallback.
Gotchas
Strings are rejected, even though they are iterable.
Iterator.concat([1], "ab")throwsTypeError: Iterator.concat called on non-objectright away, at the call, not later during iteration.nullthrows the same error. Wrap a string first:Iterator.from("bc")or[..."bc"]both worked.Bare iterators need
Iterator.from(). An object with only anext()method is not iterable, and passing one threwTypeError: #<Object> is not iterable. Built-in iterators like[1, 2, 3].values()are fine, because they are iterable too.The result is single use. Spreading the same
concatresult twice gave[1, 2, 3]the first time and[]the second. It is an iterator, not an array, so callIterator.concatagain if you need a second pass.Breaking early closes what it opened. With two generators that log in
finally, afor...ofthat broke on the first item of the second one loggedclosed awhenaran out andclosed bon thebreak. Your cleanup code runs, which is exactly what you want.
Wrap-up
Iterator.concat() is the yield* helper you kept rewriting, now with iterator helpers attached. Next time you type [...a, ...b] only to loop over it once, try Iterator.concat(a, b) instead.
Sources
- Iterator.concat() (MDN)
- Iterator Sequencing proposal (TC39)
- Finished proposals (TC39)
- Iterator browser compatibility data (MDN BCD)
This article was written with the help of AI and checked against the sources above.
Top comments (1)
Useful write-up, and the laziness test is the convincing part. One more difference from the spread version that I checked in Chrome 155: Iterator.concat reads each source when iteration reaches it, not when you call it. I made it from an array [1, 2], pushed 3 onto that array afterwards, and spreading the result gave [1, 2, 3]. [...a, ...b] would have copied [1, 2] at that moment. Handy if a source can grow, a surprise if you expected a snapshot.
Also from MDN's page: the list of iterables has to be finite and fairly short (engines limit function arguments), so for a big or unbounded list of sources it points to flatMap instead of spreading them into concat. Only tested in Chrome, not other browsers.