A client called me recently about a custom WooCommerce product sync that had started misbehaving. They were pulling roughly 15,000 SKUs from a third-party vendor. The sync would begin fine, then stall halfway, or hand back empty objects without ever crashing the server. Not a fun position for an inventory manager who just wants stock counts to match.
The developer who wrote the original script knew what he was doing, but he had hit a wall with the way he handled the data stream. He was reusing a data source he had already looped through. With JavaScript iterators, that does not work. An iterator is spent the moment you finish reading it.
Iterables vs JavaScript iterators
The naming is what trips people up. There are iterables, like arrays, maps and strings, and there are iterators. An iterable is something you can loop over as many times as you like. An array is an iterable: loop it at 9:00 AM, loop it again at 10:00 AM, and the data is still sitting there. An iterator is the stateful mechanism that walks the sequence one step at a time. Smashing Magazine ran a good breakdown of the distinction recently, and it lines up exactly with why my client’s sync was failing.
I assumed a memory leak on the PHP side of the sync at first. The real problem was that an iterator has no rewind. You cannot reset one once it is spent. It is not a static list sitting in memory, it is a sequence of events, and when it reports done it stays done.
// The wrong way: Trying to reuse a spent iterator
const bbioon_data_source = Iterator.from([ 'Product A', 'Product B' ]);
// First pass works perfectly
bbioon_data_source.forEach(item => console.log('Syncing:', item));
// Second pass? Crickets. Nothing happens because the iterator is "iterated."
bbioon_data_source.forEach(item => console.log('Checking:', item));
Following the iterator protocol means the object exposes a next() method. Each call hands back an object with a value and a done boolean. That done property only flips to true when you ask for something beyond the final element, which catches people out more often than you would expect.
Sequential access and the next() method
Calling next() advances the sequence. That is genuinely useful with big WooCommerce datasets, because you are not loading 20,000 products into memory at once, you are asking for them one at a time. The catch is that you have to respect where the internal pointer already sits.
const bbioon_product_iterator = Iterator.from([ 'SKU-001', 'SKU-002', 'SKU-003' ]);
console.log(bbioon_product_iterator.next()); // Result: { value: 'SKU-001', done: false }
console.log(bbioon_product_iterator.next()); // Result: { value: 'SKU-002', done: false }
// If we stop here, 'SKU-003' is still waiting.
// If we run a forEach now, it ONLY sees 'SKU-003'.
That was the bug. My client was peeking at the first few records with next() to check the connection, then running the full sync loop on the same iterator object. Every batch quietly skipped its first few products, and nobody could work out why the inventory never added up. Know which one you are holding: the collection, or the pointer walking through it.
Where this bites you in real projects
Knowing how JavaScript walks a sequence saves you from the kind of bug that hides for weeks in a complicated sync or a custom dashboard. Iterators give you controlled traversal. They are not the fire-and-forget for loops most of us were writing a decade ago, and they do not behave like them.
This gets messy quickly. If you are tired of debugging someone else’s code and just want the site to work, get in touch with my team. We have most likely run into it before.
Are you still reaching for a plain for-loop in your custom scripts, or have you moved over to these iteration protocols?