Refactoring callbacks to async/await

| 1 min read

Web browsers have a few functions that accept callback parameters. setTimeout and requestAnimationFrame are the first that come to mind.

If you need to do multiple calls to these functions in a row, you quickly end up in callback hell. Here's a quick tip to flatten your code with async/await.

This is extracted from an actual piece of code I was working on last week:

function callbackHell() {
  // A

  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      // B

      setTimeout(() => {
        // C
      }, 1000);
    });
  });
}

Before you know it, your code is nested 3 or more levels deep.

One thing these functions with callback parameters have in common, is that they use the callback to “run something after waiting for something else”. Sounds like a great case for async/await!

First we need to make callbackHell async to enable the await keyword inside the function.

async function callbackHell() {
  // A

  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      // B

      setTimeout(() => {
        // C
      }, 1000);
    });
  });
}

Next, we'll create a wrapper function around requestAnimationFrame to make it awaitable. Any function that returns a promise is awaitable.

function nextFrame() {
  return new Promise(resolve => {
    requestAnimationFrame(() => {
      resolve();
    });
  });
}

This can be shortened to a (kind of) oneliner:

function nextFrame() {
  return new Promise(requestAnimationFrame);
}

Now that nextFrame returns a promise, we can await it.

async function callbackHell() {
  // A

  await nextFrame();
  await nextFrame();

  // B

  setTimeout(() => {
    // C
  }, 1000);
}

Let's do the same with setTimeout.

function timeout(timeout) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve();
    }, timeout);
  });
}

Once again, a bit shorter:

function timeout(timeout) {
  return new Promise(resolve => setTimeout(resolve, timeout));
}

And our final callbackHell function:

async function callbackHell() {
  // A

  await nextFrame();
  await nextFrame();

  // B

  await timeout(1000);

  // C
}

No more callback hell, just one straight line!

Caveats

Refactoring to async/await only works if you don't care about the return value of these functions.

For example, setTimeout returns an ID that can be used to cancel the timeout.

const timeout = setTimeout(() => {}, 1000);

clearTimeout(timeout);

This piece of code can't be refactored to async/await, since await only returns a value after the timeout has completed.


Webmentions ?