How to rewrite a callback function in Promise form and async/await form in JavaScript

Corey Cleary - Jan 16 '19 - - Dev Community

Originally published at coreycleary.me. This is a cross-post from my content blog. I publish new content every week or two, and you can sign up to my newsletter if you'd like to receive my articles directly to your inbox! I also regularly send cheatsheets and other freebies!

"You should really use Promises or async/await here to make this more readable"

How many times have you posted some code snippet when trying to get an answer to your question, and someone ends up pestering you about this? Now, on top of whatever problem you already have with your code, you have another thing you need to learn and "fix"...

Or what about dealing with refactoring an existing, callback-based codebase at work? How do you convert them to native JavaScript Promises? It would be so great to be able to develop using modern JavaScript and start making use of the async/await functionality...

If you knew how to avoid callbacks, you could post your code online when asking for help without people asking you to rewrite it and not actually answering your question.

And if you were refactoring an existing codebase, that code would be more readable, you could avoid the "callback hell" people still seem to talk about even in 2019 when Promises have had support in many browsers and Node for years now, and async/await is supported by many versions as well...

The fix

Let's go over how to convert those old-school callbacks to Promises and to async/await versions.

Here's the link to the code demonstrating the callback -> Promise and callback -> async/await versions.

Callback version

const callbackFn = (firstName, callback) => {
  setTimeout(() => {
    if (!firstName) return callback(new Error('no first name passed in!'))

    const fullName = `${firstName} Doe`

    return callback(fullName)
  }, 2000)
}

callbackFn('John', console.log)
callbackFn(null, console.log)
Enter fullscreen mode Exit fullscreen mode

You'll notice here that we're using the setTimeout() function in order to make our function asynchronous. In addition to setTimeout(), other asynchronous operations you're likely to see in the real-world are: AJAX and HTTP calls, database calls, filesystem calls (in the case of Node, if no synchronous version exists), etc.

In this function, we "reject" it if the first name argument is null. When we do pass in the firstName argument, the callback function (almost always the last argument in a callback-based function's argument list) gets called and returns our value after the 2 seconds set in setTimeout().

If we don't pass in a callback, we get a TypeError: callback is not a function error.

Promise version

And here's the Promise-based version of that function:

const promiseFn = firstName => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (!firstName) reject(new Error('no first name passed in!'))

      const fullName = `${firstName} Doe`  

      resolve(fullName)
    }, 2000)
  })
}

promiseFn('Jane').then(console.log)
promiseFn().catch(console.log)
Enter fullscreen mode Exit fullscreen mode

Converting to a Promise-based function is actually pretty simple. Look at the below diagram for a visual explanation:

First, we remove the callback argument. Then we add the code to return a new Promise from our Promise-based function. The error callback becomes a reject, while the "happy path" callback becomes a resolve.

When we call the promiseFn, the result from the happy path will show up in the .then(), while the error scenario will show up in the .catch().

The great thing about having our function in Promise form is that we don't actually need to "make it an async/await version" if we don't want to. When we call/execute the function, we can simply use the async/await keyword, like so:

const result = (async () => {
  try {
    console.log(await promiseFn('Jim')) 
  } catch (e) {
    console.log(e)
  }

  try {
    console.log(await promiseFn()) 
  } catch (e) {
    console.log(e)
  }
})()
Enter fullscreen mode Exit fullscreen mode

Side note: here I wrapped the function call in an IIFE - that's what that (async () => {....})() is if you've never seen it. This is simply because we need to wrap the await call in a function that uses the async keyword, and we also want to "immediately invoke" the function (IIFE = "Immediately Invoked Function Execution") in order to call it.

Here, there are no callbacks, no .then()'s or .catch()'s, we just use a try/catch block and call the promiseFn(). Promise rejections will be caught by the catch block.

Note: async/await is available in most semi-recent releases of the major browsers, with the exception of Internet Explorer. Node has had support for the feature since version 7.6.0

async/await version

But what if we wanted to convert a callback function directly to an async/await version of that function? Without using Promises directly?

async/await is syntactic sugar around Promises, so it uses them under the hood. Here's how you can convert it:

const timeout = ms => {
  return new Promise(resolve => setTimeout(resolve, ms))
}

const asyncAwaitFn = async firstName => {
  await timeout(2000) // using timeout like this makes it easier to demonstrate callback -> async/await conversion

  if (!firstName) throw new Error('no first name passed in!')

  const fullName = `${firstName} Doe`

  return fullName
}

const res = (async () => {
  try {
    console.log(await asyncAwaitFn('Jack')) 
  } catch (e) {
    console.log(e)
  }

  try {
    console.log(await asyncAwaitFn()) 
  } catch (e) {
    console.log(e)
  }
})()
Enter fullscreen mode Exit fullscreen mode

Use the below diagram to understand how to go from callback to async:


Similar to converting to the Promise-based version, we get rid of the callback passed in to the original function, as well as that argument call within the body of the function. Next, we add the async keyword to the beginning of the function declaration. And finally, when we hit the error scenario, we throw an Error, which results in a rejected Promise (caught in the catch block when we call the function), and simply return the fullName in the happy path scenario.

Note that async functions all return Promises, so when you use return you are just resolving the Promise.

Wrapping up

Next time you need to convert a callback-based function to a Promise-based one or an async/await-based versions, use the visual diagrams from this post to quickly and easily do so. And if you need some code to play around with to help the concepts settle some more, here's the link again to the code demonstrating the callback -> Promise, and callback -> async/await versions.

Callback hell is now gone!

I have plenty more content planned for the future, so if you found this helpful and want to receive it directly to your inbox without having to remember to check back here, here's the link again for the newsletter!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .