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.