Day 5: async/await, Generators & Iterators Internals
Understand async/await as syntactic sugar over generators + promises, and know how the iterator protocol powers for...of, spread, and generators.
Study
Concepts
The iterator protocol
An object is "iterable" if it implements `[Symbol.iterator]()`, a method returning an iterator object with a `.next()` method. Calling `.next()` returns `{ value, done }`. `for...of`, spread (`...`), destructuring, and `Array.from` all work by repeatedly calling this protocol — it is the single mechanism behind all of them.
Arrays, Strings, Maps, Sets, and Node streams all implement this protocol natively, which is why they all support `for...of` for free.
Generators: pausable functions
A `function*` returns a Generator — an object that is both iterable AND an iterator. Calling `.next()` runs the function body until the next `yield`, then pauses execution (freezing the entire call stack of that function) and returns the yielded value. Calling `.next(arg)` again resumes exactly where it left off, with `arg` becoming the result of the `yield` expression.
This pause/resume ability is the low-level primitive that async/await is built on: you can write a generator that yields Promises, then use a driver function that calls `.next()` again only once each yielded Promise resolves.
async/await = generator + promise driver, automated
`async function` compiles (conceptually) to a generator whose body yields at every `await`, wrapped in a driver loop that automatically calls `.next()` with the resolved value, or `.throw()` with the rejection reason. This is why `await` looks synchronous but is still fully non-blocking — the Call Stack unwinds at each `await`, exactly like it does at each `yield`.
An `async function` always returns a Promise, and a `throw` inside it becomes a rejection — so try/catch works naturally across async boundaries, unlike with raw callbacks.
See It
Visualizations
Visualization
How a generator-based async driver works
runs body until first `yield fetch(...)`
driver attaches .then to it, call stack unwinds
driver calls gen.next(resolvedValue)
resolvedValue becomes the result of `yield`
driver resolves its own returned Promise
Build It
Code Examples
A minimal async/await, built from a generator + driver
function runAsync(generatorFn) {
return new Promise((resolve, reject) => {
const gen = generatorFn();
function step(nextFn, arg) {
let result;
try {
result = nextFn(arg); // gen.next(arg) or gen.throw(arg)
} catch (err) {
return reject(err);
}
const { value, done } = result;
if (done) return resolve(value);
Promise.resolve(value).then(
(v) => step(gen.next.bind(gen), v),
(e) => step(gen.throw.bind(gen), e)
);
}
step(gen.next.bind(gen), undefined);
});
}
function fakeFetch(value, delay = 100) {
return new Promise((res) => setTimeout(() => res(value), delay));
}
// Write it with 'yield' exactly where you'd write 'await'
runAsync(function* () {
const user = yield fakeFetch({ id: 1, name: 'Ali' });
const posts = yield fakeFetch([`post by ${user.name}`]);
console.log(user, posts);
return 'done';
}).then((r) => console.log('runAsync resolved with', r));This is essentially what Babel generated before native async/await existed. Compare it line-by-line with the equivalent async/await version below.
The equivalent async/await (what it desugars to)
async function loadUserAndPosts() {
const user = await fakeFetch({ id: 1, name: 'Ali' });
const posts = await fakeFetch([`post by ${user.name}`]);
console.log(user, posts);
return 'done';
}
function fakeFetch(value, delay = 100) {
return new Promise((res) => setTimeout(() => res(value), delay));
}
loadUserAndPosts().then((r) => console.log('resolved with', r));Remember
Key Takeaways
- for...of, spread, and destructuring all run on the iterator protocol: {value, done} from .next().
- Generators can pause and resume a call stack — the mechanic async/await hides behind clean syntax.
- An async function always returns a Promise; throw inside it becomes a rejection.
- await unwraps a Promise's value the same way yield does inside a generator/promise driver.
- Understanding this mapping makes debugging async stack traces and transpiled output far less mysterious.
Do It
Practice
- 1Write a custom iterable object (not an array) that yields Fibonacci numbers via Symbol.iterator, then loop it with for...of.
- 2Convert one of your own async/await functions into raw generator + runAsync driver form by hand.
- 3Add error handling to runAsync and prove try/catch works across an awaited rejection.