5 Async/Await Design Patterns for Cleaner Async Logic

At Mastering JS, we love async/await. You might even say we wrote the book on async/await. Here's 5 design patterns we use regularly.

Async forEach()

Do not use an async callback with forEach(). In general, the way to simulate forEach() with async functions is to use await Promise.all([arr.map(callback)])

const values = [10, 50, 100];

// Do this:
await Promise.all(values.map(async v => {
  await new Promise(resolve => setTimeout(resolve, v));
  console.log('Slept for', v, 'ms');
}));

// Not this:
values.forEach(async v => {
  await new Promise(resolve => setTimeout(resolve, v));
  console.log('Slept for', v, 'ms');
});

return await

Async/await works with try/catch... almost. There's a gotcha. If you await on a promise that rejects, JavaScript throws an error that you can catch. But if you return a promise that rejects, that ends up as an unhandled promise rejection.

const p = Promise.reject(new Error('Oops!'));

try {
  await p;
} catch (err) {
  console.log('This runs...');
}

try {
  return p;
} catch (err) {
  console.log('This does NOT run!');
}

There are a few workarounds for this quirk, but one approach we like is using return await.

try {
  return await p;
} catch (err) {
  console.log('This runs!');
}

Delayed await

Sometimes you want to call an async function, do something else, and then await on the async function. Promises are just variables in JavaScript, so you can call an async function, get the promise response, and await on it later.

const ee = new EventEmitter();

// Execute the function, but don't `await` so we can `setTimeout()`
const p = waitForEvent(ee, 'test');

setTimeout(() => ee.emit('test'), 1000);

// Wait until `ee` emits a 'test' event
await p;

async function waitForEvent(ee, name) {
  await new Promise(resolve => {
    ee.once(name, resolve);
  });
}

await with Promise Chaining

We recommend using Axios over fetch(), but in some cases you may need to use fetch(). And fetch() famously requires you to asynchronously parse the response body. Here's how you can make a request with fetch() and parse the response body with 1 await.

const res = await fetch('/users').then(res => res.json());

Another quirk of fetch() is that it doesn't throw an error if the server responds with an error code, like 400. Here's how you can make fetch() throw a catchable error if the response code isn't in the 200 or 300 range.

const res = await fetch('/users').
  then(res => {
    if (res.status < 200 || res.status >= 400) {
      throw new Error('Server responded with status code ' + res.status);
    }
    return res;
  }).
  then(res => res.json());

Waiting for Events

Event emitters are a common pattern in JavaScript, but they don't work well with async/await because they're not promises. Here's how you can await on an event from a Node.js event emitter.

const ee = new EventEmitter();

setTimeout(() => ee.emit('test'), 1000);

// Wait until `ee` emits a 'test' event
await new Promise(resolve => {
  ee.once('test', resolve);
});

18