DEV Community

Mastering JS
Mastering JS

Posted on

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');
});
Enter fullscreen mode Exit fullscreen mode

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!');
}
Enter fullscreen mode Exit fullscreen mode

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!');
}
Enter fullscreen mode Exit fullscreen mode

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);
  });
}
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

Discussion (2)

Collapse
saroj990 profile image
saroj sasmal

Nice Article!. IMO await with promise chaining is not ideal and should not be used at all. By default await statement will wait for the execution of the promise, so adding a then block is not required.

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());
Enter fullscreen mode Exit fullscreen mode

The above statement can be simplified with a single await within a try/catch block like following

try {
  const res = await fetch('/users');
  if (res.status < 200 || res.status >= 400) {
    throw new Error('Server responded with status code ' + res.status);
  }
  const data = res.json();
  // do something with data
} catch(err) {
  // handle error
}
Enter fullscreen mode Exit fullscreen mode
Collapse
masteringjs profile image
Mastering JS Author

The problem is that you need to do await res.json() as well, but otherwise your approach works. I think the await fetch().then(res => res.json()) pattern is common enough that it's worth mentioning.