ES6 generators transforming callbacks to iterators
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Before async and await became the standard style for asynchronous JavaScript, generators were often used to turn nested callbacks into code that looked sequential. The core idea is simple: a generator pauses at each async step, and a small runner resumes it when the next value is ready.
Start with the Callback Problem
Callback-based code spreads control flow across several nested functions. Error handling and data dependencies get harder to follow as the sequence grows.
This is manageable with two steps, but it gets messy quickly.
Use a Generator as a Pause-and-Resume State Machine
A generator returns an iterator. Each yield pauses execution and hands control back to the caller. When the caller later calls next(value), that value becomes the result of the suspended yield expression.
That pause-and-resume behavior is exactly what makes generators useful for async orchestration.
Wrap Callback APIs and Run the Generator
The common pattern is to wrap old callback APIs in promises, then write a runner that feeds resolved values back into the generator.
The generator now reads like straight-line logic even though the work is still asynchronous underneath.
Why This Pattern Still Matters
Modern JavaScript code should usually use async and await for this job. Even so, generator runners still matter for two reasons:
- older codebases may already use them
- they help explain how sequential-looking async control flow can be built from iterators and promises
Understanding this pattern makes libraries such as co much less mysterious and makes async functions easier to appreciate, because async and await solve the same control-flow problem more directly.
Common Pitfalls
The biggest mistake is thinking a generator is asynchronous by itself. It is not. A generator only pauses and resumes. You still need a runner or some external code to drive the iterator.
Another common bug is forgetting to send rejected promises into iterator.throw. If the runner only handles successful values, errors disappear or surface in the wrong place.
People also mix callbacks, promises, and generators without a clear boundary. That creates code that is harder to debug than the original callback chain. Once you wrap an API in a promise, keep the rest of the flow consistent.
Finally, do not introduce generators for new async code unless there is a specific reason. async and await are usually clearer.
Summary
- Generators can turn callback-heavy async flows into sequential-looking code.
- A generator pauses at
yield, and a runner resumes it with the next value. - Wrapping callback APIs in promises makes generator runners much easier to write.
- This pattern is mainly useful for understanding or maintaining older async JavaScript.
- For new code, prefer
asyncandawaitunless you specifically need iterator behavior.

