RoadmapDay 5 / 80
JavaScript CoreMonth 1 · Week 1

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.

Mark this day complete

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

driver calls gen.next()

runs body until first `yield fetch(...)`

yields a Promise

driver attaches .then to it, call stack unwinds

Promise resolves

driver calls gen.next(resolvedValue)

generator resumes

resolvedValue becomes the result of `yield`

repeat until { done: true }

driver resolves its own returned Promise

Build It

Code Examples

A minimal async/await, built from a generator + driver

js
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)

js
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

  1. 1Write a custom iterable object (not an array) that yields Fibonacci numbers via Symbol.iterator, then loop it with for...of.
  2. 2Convert one of your own async/await functions into raw generator + runAsync driver form by hand.
  3. 3Add error handling to runAsync and prove try/catch works across an awaited rejection.