09. Async Generators

Async generators a mixture of async functions and generators. Normal (synchronous) generators help with implementing synchronous iterables. Asynchronous generators do the same for asynchronous iterables. Async generators allow you to create async iterator factories.

  • An async generator returns a generator object which follows iterator specification.

  • Each invocation genObj.next() returns a Promise for an object {value, done} that wraps a yielded value.

  • Generator doesn't have a natural end - you can't check the end directly and should await for promise to be resolved to get done

// Note the * after "function"
async function* asyncRandomNumbers() {
  // This is a web service that returns a random number
  const url =
    "https://www.random.org/decimal-fractions/?num=1&dec=10&col=1&format=plain&rnd=new";

  while (true) {
    const response = await fetch(url);
    const text = await response.text();
    yield Number(text);
  }
}

async function example() {
  for await (const number of asyncRandomNumbers()) {
    console.log(number);
    // use break to stop it because generator doesn't have natural end.
    if (number > 0.95) break;
  }
}

Like all for-loops, you can break whenever you want. This results in the loop calling iterator.return(), which causes the generator to act as if there was a return statement after the current (or next) yield.

Async generator execution:

  • Каждый вызов next() возвращает Promise.

  • Последующий вызов next() вернет следующий Promise безотносительно результата предыдущего Promise (нет необходимости ждать, пока предыдущий Promise будет установлен).

  • next() вызовы являются неблокирующими.

  • Узнать, закончился ли генератор, можно только дождавшись когда Promise будет установлен и проверив значение done.

  • У асинхронного генератора нет понятия "длинны" или "количества доступных элементов".

  • Поэтому асинхронные генераторы не поддерживают spread оператор.

  • yield x fulfills the “current” Promise with {value: x, done: false}.

  • throw err rejects the “current” Promise with err.

  • In normal generators, next() can throw exceptions. In async generators, next() can reject the Promise it returns.

await in async generators

You can use await and for-await-of inside async generators. For example:

async function* prefixLines(asyncIterable) {
  for await (const line of asyncIterable) {
    yield "> " + line;
  }
}

One interesting aspect of combining await and yield is that await can’t stop yield from returning a Promise, but it can stop that Promise from being settled.

Fetch number of elements

Retrieving Promises to be processed via Promise.all(). If you know how many elements there are in an async iterable, you don’t need to check done.

async function* createAsyncIterable(syncIterable) {
  for (const elem of syncIterable) {
    yield elem;
  }
}

const asyncGenObj = createAsyncIterable(["a", "b"]);
const [{ value: v1 }, { value: v2 }] = await Promise.all([
  asyncGenObj.next(),
  asyncGenObj.next()
]);

console.log(v1, v2); // a b

Async data operations

Async generators as sinks for data, where you don’t always need to know when they are done:

const writer = openFile("someFile.txt");
writer.next("hello"); // don’t wait
writer.next("world"); // don’t wait
await writer.return(); // wait for file to close

Async generators in depth

a rough approximation of how async generators work:

const BUSY = Symbol('BUSY');
const COMPLETED = Symbol('COMPLETED');
function asyncGenerator() {
    const settlers = [];
    let step = 0;
    return {
        [Symbol.asyncIterator]() {
            return this;
        },
        next() {
            return new Promise((resolve, reject) => {
                settlers.push({resolve, reject});
                this._run();
            });
        }
        _run() {
            setTimeout(() => {
                if (step === BUSY || settlers.length === 0) {
                    return;
                }
                const currentSettler = settlers.shift();
                try {
                    switch (step) {
                        case 0:
                            step = BUSY;
                            console.log('Start');
                            doSomethingAsync()
                            .then(result => {
                                currentSettler.resolve({
                                    value: 'Result: '+result,
                                    done: false,
                                });
                                // We are not busy, anymore
                                step = 1;
                                this._run();
                            })
                            .catch(e => currentSettler.reject(e));
                            break;
                        case 1:
                            console.log('Done');
                            currentSettler.resolve({
                                value: undefined,
                                done: true,
                            });
                            step = COMPLETED;
                            this._run();
                            break;
                        case COMPLETED:
                            currentSettler.resolve({
                                value: undefined,
                                done: true,
                            });
                            this._run();
                            break;
                    }
                }
                catch (e) {
                    currentSettler.reject(e);
                }
            }, 0);
        }
    }
}

Last updated