Advanced JavaScript Iterators Complete Guide

Master the JavaScript iterator protocol for custom collection traversal. Covers Symbol.iterator, iterator result protocol, iterable consumers, lazy iterators, stateful iteration, bidirectional iterators, composable iterator utilities, and integration with destructuring, spread, and for-of.

JavaScriptadvanced
18 min read

The iterator protocol defines a standard way to produce a sequence of values. Any object implementing [Symbol.iterator]() becomes iterable, unlocking for...of, spread syntax, destructuring, Array.from(), and all collection-consuming APIs.

For generator-based iterators, see JavaScript Generators Deep Dive Full Guide.

The Iterator Protocol in Depth

javascriptjavascript
// An ITERATOR is any object with a next() method returning { value, done }
// An ITERABLE is any object with a [Symbol.iterator]() method that returns an iterator
 
// Manual iterator (no generators)
class RangeIterator {
  #current;
  #end;
  #step;
 
  constructor(start, end, step = 1) {
    this.#current = start;
    this.#end = end;
    this.#step = step;
  }
 
  next() {
    if (this.#current < this.#end) {
      const value = this.#current;
      this.#current += this.#step;
      return { value, done: false };
    }
    return { value: undefined, done: true };
  }
 
  // An iterator can be its own iterable
  [Symbol.iterator]() {
    return this;
  }
}
 
const range = new RangeIterator(0, 5);
for (const n of range) {
  console.log(n); // 0, 1, 2, 3, 4
}
 
// ITERABLE CLASS (returns fresh iterator each time)
class Range {
  #start;
  #end;
  #step;
 
  constructor(start, end, step = 1) {
    this.#start = start;
    this.#end = end;
    this.#step = step;
  }
 
  [Symbol.iterator]() {
    let current = this.#start;
    const end = this.#end;
    const step = this.#step;
 
    return {
      next() {
        if (current < end) {
          const value = current;
          current += step;
          return { value, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
 
  get length() {
    return Math.ceil((this.#end - this.#start) / this.#step);
  }
}
 
const r = new Range(0, 10, 2);
console.log([...r]); // [0, 2, 4, 6, 8]
console.log([...r]); // [0, 2, 4, 6, 8] -- works again because new iterator each time
 
// ALL BUILT-IN CONSUMERS OF THE ITERABLE PROTOCOL
const nums = new Range(1, 4);
 
// for...of
for (const n of nums) { /* 1, 2, 3 */ }
 
// Spread
const arr = [...nums]; // [1, 2, 3]
 
// Destructuring
const [a, b] = nums; // a=1, b=2
 
// Array.from
const fromArr = Array.from(nums); // [1, 2, 3]
 
// Map, Set constructors
const s = new Set(nums);       // Set {1, 2, 3}
const m = new Map([[1,"a"],[2,"b"]]); // from iterable of [key, value]
 
// Promise.all, Promise.race
// yield* (in generators)
// new Int32Array(nums), new Uint8Array(nums) etc.

Stateful and Closeable Iterators

javascriptjavascript
// RETURN METHOD: cleanup when iteration is interrupted
class FileLineIterator {
  #lines;
  #index = 0;
  #closed = false;
  #filename;
 
  constructor(filename, lines) {
    this.#filename = filename;
    this.#lines = lines;
    console.log(`Opened: ${filename}`);
  }
 
  next() {
    if (this.#closed || this.#index >= this.#lines.length) {
      this.#cleanup();
      return { value: undefined, done: true };
    }
    return { value: this.#lines[this.#index++], done: false };
  }
 
  // Called by for...of on break/return/throw
  return(value) {
    this.#cleanup();
    return { value, done: true };
  }
 
  #cleanup() {
    if (!this.#closed) {
      this.#closed = true;
      console.log(`Closed: ${this.#filename}`);
    }
  }
 
  [Symbol.iterator]() {
    return this;
  }
}
 
const fileIter = new FileLineIterator("data.csv", [
  "name,age", "Alice,30", "Bob,25", "Charlie,35"
]);
 
for (const line of fileIter) {
  console.log(line);
  if (line.startsWith("Bob")) break; // Triggers return()
}
// Output: Opened: data.csv, name,age, Alice,30, Bob,25, Closed: data.csv
 
// STATEFUL ITERATOR WITH PEEK AND PUSHBACK
class PeekableIterator {
  #source;
  #buffer = [];
  #done = false;
 
  constructor(iterable) {
    this.#source = iterable[Symbol.iterator]();
  }
 
  next() {
    if (this.#buffer.length > 0) {
      return { value: this.#buffer.shift(), done: false };
    }
 
    if (this.#done) {
      return { value: undefined, done: true };
    }
 
    const result = this.#source.next();
    if (result.done) {
      this.#done = true;
    }
    return result;
  }
 
  peek() {
    if (this.#buffer.length > 0) {
      return { value: this.#buffer[0], done: false };
    }
 
    const result = this.#source.next();
    if (result.done) {
      this.#done = true;
      return result;
    }
 
    this.#buffer.push(result.value);
    return { value: result.value, done: false };
  }
 
  pushback(value) {
    this.#buffer.unshift(value);
    this.#done = false;
  }
 
  [Symbol.iterator]() {
    return this;
  }
}
 
const peekable = new PeekableIterator([10, 20, 30]);
console.log(peekable.peek());  // { value: 10, done: false }
console.log(peekable.peek());  // { value: 10, done: false } (still same)
console.log(peekable.next());  // { value: 10, done: false } (consumed)
console.log(peekable.next());  // { value: 20, done: false }
peekable.pushback(15);
console.log(peekable.next());  // { value: 15, done: false } (pushed back)
console.log(peekable.next());  // { value: 30, done: false }

Composable Iterator Utilities

javascriptjavascript
// Functional iterator utilities that work with any iterable
 
const Iter = {
  // Transform each element
  map(iterable, fn) {
    return {
      [Symbol.iterator]() {
        const iter = iterable[Symbol.iterator]();
        return {
          next() {
            const { value, done } = iter.next();
            return done ? { value: undefined, done } : { value: fn(value), done: false };
          }
        };
      }
    };
  },
 
  // Keep elements matching predicate
  filter(iterable, predicate) {
    return {
      [Symbol.iterator]() {
        const iter = iterable[Symbol.iterator]();
        return {
          next() {
            while (true) {
              const { value, done } = iter.next();
              if (done) return { value: undefined, done: true };
              if (predicate(value)) return { value, done: false };
            }
          }
        };
      }
    };
  },
 
  // Take first N elements
  take(iterable, n) {
    return {
      [Symbol.iterator]() {
        const iter = iterable[Symbol.iterator]();
        let count = 0;
        return {
          next() {
            if (count >= n) return { value: undefined, done: true };
            count++;
            return iter.next();
          }
        };
      }
    };
  },
 
  // Concatenate multiple iterables
  concat(...iterables) {
    return {
      [Symbol.iterator]() {
        let index = 0;
        let current = iterables[0]?.[Symbol.iterator]();
        return {
          next() {
            while (current) {
              const result = current.next();
              if (!result.done) return result;
              index++;
              current = iterables[index]?.[Symbol.iterator]();
            }
            return { value: undefined, done: true };
          }
        };
      }
    };
  },
 
  // Zip multiple iterables into tuples
  zip(...iterables) {
    return {
      [Symbol.iterator]() {
        const iters = iterables.map(it => it[Symbol.iterator]());
        return {
          next() {
            const results = iters.map(it => it.next());
            if (results.some(r => r.done)) {
              return { value: undefined, done: true };
            }
            return { value: results.map(r => r.value), done: false };
          }
        };
      }
    };
  },
 
  // Flatten one level
  flat(iterable) {
    return {
      [Symbol.iterator]() {
        const outer = iterable[Symbol.iterator]();
        let inner = null;
        return {
          next() {
            while (true) {
              if (inner) {
                const result = inner.next();
                if (!result.done) return result;
                inner = null;
              }
              const outerResult = outer.next();
              if (outerResult.done) return { value: undefined, done: true };
              if (outerResult.value?.[Symbol.iterator]) {
                inner = outerResult.value[Symbol.iterator]();
              } else {
                return { value: outerResult.value, done: false };
              }
            }
          }
        };
      }
    };
  },
 
  // Reduce to single value
  reduce(iterable, fn, initial) {
    let acc = initial;
    for (const item of iterable) {
      acc = fn(acc, item);
    }
    return acc;
  },
 
  // Collect into array
  toArray(iterable) {
    return [...iterable];
  }
};
 
// Composing utilities
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
 
const result = Iter.toArray(
  Iter.take(
    Iter.map(
      Iter.filter(data, n => n % 2 === 0),
      n => n * n
    ),
    3
  )
);
 
console.log(result); // [4, 16, 36]

Async Iterator Protocol

javascriptjavascript
// ASYNC ITERABLES use Symbol.asyncIterator and return promises from next()
 
class AsyncStream {
  #items;
  #delayMs;
 
  constructor(items, delayMs = 100) {
    this.#items = items;
    this.#delayMs = delayMs;
  }
 
  [Symbol.asyncIterator]() {
    let index = 0;
    const items = this.#items;
    const delayMs = this.#delayMs;
 
    return {
      async next() {
        if (index >= items.length) {
          return { value: undefined, done: true };
        }
 
        await new Promise(r => setTimeout(r, delayMs));
        return { value: items[index++], done: false };
      },
 
      async return() {
        console.log("Async iterator cleanup");
        return { value: undefined, done: true };
      }
    };
  }
}
 
async function consumeStream() {
  const stream = new AsyncStream(["alpha", "beta", "gamma"]);
 
  for await (const item of stream) {
    console.log(item);
  }
}
 
// PAGINATED API ITERATOR
class PaginatedAPI {
  #baseUrl;
  #pageSize;
 
  constructor(baseUrl, pageSize = 20) {
    this.#baseUrl = baseUrl;
    this.#pageSize = pageSize;
  }
 
  [Symbol.asyncIterator]() {
    let page = 1;
    let exhausted = false;
    const baseUrl = this.#baseUrl;
    const pageSize = this.#pageSize;
 
    return {
      async next() {
        if (exhausted) {
          return { value: undefined, done: true };
        }
 
        // Simulated API call
        const response = await simulateAPI(baseUrl, page, pageSize);
 
        if (response.items.length < pageSize) {
          exhausted = true;
        }
 
        page++;
        return { value: response.items, done: false };
      }
    };
  }
}
 
async function simulateAPI(url, page, size) {
  const totalItems = 45;
  const start = (page - 1) * size;
  const items = [];
  for (let i = start; i < Math.min(start + size, totalItems); i++) {
    items.push({ id: i + 1, name: `Item ${i + 1}` });
  }
  return { items, page, totalPages: Math.ceil(totalItems / size) };
}
 
// Consume all pages
async function getAllItems() {
  const api = new PaginatedAPI("/api/items", 20);
  const allItems = [];
 
  for await (const page of api) {
    allItems.push(...page);
    console.log(`Fetched ${allItems.length} items`);
 
    if (page.length === 0) break;
  }
 
  return allItems;
}
ProtocolMethodReturnsConsumerUse Case
Iteratornext(){ value, done }for...of, spreadSynchronous sequences
Async Iteratorasync next()Promise<{ value, done }>for await...ofAsync data streams
Iterable[Symbol.iterator]()IteratorAll sync consumersCustom collections
Async Iterable[Symbol.asyncIterator]()Async IteratorAsync consumersPaginated APIs, streams
Rune AI

Rune AI

Key Insights

  • The iterable protocol requires Symbol.iterator returning an iterator; the iterator protocol requires next() returning { value, done }: Separating these lets iterables produce fresh iterators for repeated traversal
  • Implement return() on iterators that hold resources to ensure cleanup when consumers exit early via break, return, or throw: Without return(), resources leak on incomplete iteration
  • Composable iterator utilities (map, filter, take, zip, flat) enable lazy pipelines without intermediate arrays: Each utility wraps an iterator and transforms values on demand
  • The async iterator protocol mirrors the sync version but next() returns promises, consumed via for await...of: This is ideal for paginated APIs, real-time streams, and network data
  • PeekableIterator with pushback enables look-ahead parsing and tokenization that standard iterators cannot express: Buffer consumed values and re-emit them for subsequent consumers
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between an iterator and an iterable?

n iterable is any object with a `[Symbol.iterator]()` method that returns an iterator. An iterator is any object with a `next()` method that returns `{ value, done }`. Arrays, strings, Maps, and Sets are iterables because they have `[Symbol.iterator]()`. The iterator they return is a separate object that tracks position. A common pattern is making an iterator also iterable (returning itself from `[Symbol.iterator]()`), which allows it to work with `for...of` directly. The distinction matters because iterables can produce multiple independent iterators for repeated traversal.

When should I implement return() on my iterator?

Implement `return()` whenever your iterator holds resources that need cleanup: open files, database connections, network sockets, event subscriptions, or acquired locks. The `for...of` loop calls `return()` when exiting early via `break`, `return`, or `throw`. Without `return()`, resources leak when consumers stop iterating before completion. Generator functions automatically handle this through `finally` blocks. For manual iterators, `return()` should release resources and return `{ value: undefined, done: true }`.

How do iterator helpers (TC39 proposal) change iteration?

The Iterator Helpers proposal (Stage 3) adds methods like `.map()`, `.filter()`, `.take()`, `.drop()`, `.flatMap()`, `.reduce()`, `.toArray()`, and `.forEach()` directly to the iterator prototype. This eliminates the need for utility libraries or custom wrapper functions. Instead of `Iter.take(Iter.map(iter, fn), 5)`, you write `iter.map(fn).take(5).toArray()`. All helper methods return lazy iterators (except terminal methods like `reduce` and `toArray`), preserving the lazy evaluation benefit.

Can I make any object iterable?

Yes. Add a `[Symbol.iterator]()` method that returns an object with a `next()` method. This works on class instances, plain objects, and even primitives via prototype extension (not recommended). For example, making a linked list iterable: add `[Symbol.iterator]()` that walks the nodes. Making a binary tree iterable: add `[Symbol.iterator]()` that performs in-order traversal. Once iterable, the object works with `for...of`, spread, destructuring, `Array.from()`, and all other iterable consumers.

Conclusion

The iterator protocol provides a universal interface for sequential data access. Custom iterators, composable utilities, and the async iterator protocol enable elegant handling of collections, streams, and lazy computation. For building custom iterable data structures, continue to Creating JavaScript Custom Iterables Full Guide. For async flows built on these protocols, see Handling Async Flows with JS Generator Functions.