:pencil: Today I Wrote

A JavaScript function that recursively resolves promises

programming JavaScript javascript async-await

Async/await is awesome because it enables asynchronous, promise-based behavior to be written in a cleaner, more imperative-looking style:

async function computeAsyncStuff() {
  const foo = await computeFoo();
  const bar = await computeBar();
  return foo + bar;
}

Want to run things in parallel? Piece of cake, with a nice destructuring assignment as a bonus:

async function computeAsyncStuffInParallel() {
  const [ foo, bar ] = await Promise.all([
    computeFoo(),
    computeBar()
  ]);

  return foo + bar;
}

But… oh no, it doesn’t work with objects:

async function computeAsyncObject() {
  const { foo, bar } = await {
    foo: computeFoo(),
    bar: computeBar()
  };

  console.log(foo); // Promise { ... }
  console.log(bar); // Promise { ... }

  return foo + bar; // '[object Promise][object Promise]'
}

So I wrote a small function to resolve nested structures of plain arrays and objects:

async function resolve(value) {
  // Await the value in case it's a promise.
  const resolved = await value;

  if (isPlainObject(resolved)) {
    const entries = Object.entries(resolved);
    const resolvedEntries = entries.map(
      // Recursively resolve object values.
      async ([ key, value ]) => [ key, await resolve(value) ]
    );
    return Object.fromEntries(
      await Promise.all(resolvedEntries)
    );
  } else if (Array.isArray(resolved)) {
    // Recursively resolve array values.
    return Promise.all(resolved.map(resolve));
  }

  return resolved;
}

function isPlainObject(value) {
  return typeof value === 'object' &&
    value !== null &&
    value.constructor === Object;
}

Now you can do this:

async function computeAsyncObject() {
  const { foo, bar } = await resolve({
    foo: computeFoo(),
    bar: computeBar()
  });

  console.log(foo); // 14
  console.log(bar); // 28

  return foo + bar; // 42
}

You can even do this with arbitrarily nested structures as long as they’re only arrays and plain objects:

async function computeAsyncStructure() {
  const { foo, bar: [ baz, qux ] } = await resolve({
    foo: computeFoo(),
    bar: Promise.resolve([
      computeBaz(),
      computeQux()
    ])
  });

  return foo + baz + qux;
}

If you like Lodash, here’s another version taking advantage of its utility functions:

const {
  isArray,
  isPlainObject,
  zipObject
} = require('lodash');

async function resolve(value) {
  // Await the value in case it's a promise.
  const resolved = await value;

  if (isPlainObject(resolved)) {
    const keys = Object.keys(resolved);
    // Recursively resolve object values.
    const resolvedValues = await Promise.all(
      Object.values(resolved).map(resolve)
    );
    return zipObject(keys, resolvedValues);
  } else if (isArray(resolved)) {
    // Recursively resolve array values.
    return Promise.all(resolved.map(resolve));
  }

  return resolved;
}

The world awaits.