loading...

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

ccleary00 profile image Corey Cleary Originally published at coreycleary.me ・5 min read

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)

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)

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)
  }
})()

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)
  }
})()

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!

Discussion

pic
Editor guide
Collapse
aligoren profile image
Ali GOREN

I remember this picture

https://cdn-images-1.medium.com/max/823/1*Co0gr64Uo5kSg89ukFD2dw.jpeg

I don't know if there a promise hell example. Thanks for this great article.

Collapse
quantumsheep profile image
Nathanael Demacon

Promise hell can't exist, that's why it's so nice!

const call_api = (url) => fetch(url).then(res => res.json())
const heaven = (url) => call_api(url).then(data => console.log(data))

heaven('/api/users')
  .then(() => heaven('/api/posts'))
  .then(([{ id }]) => heaven(`/api/posts/reactions?post=${id}`))
  .then(({ post: id }) => heaven(`/api/comments?post=${id}`))

See? It's really clean 😃

Collapse
ccleary00 profile image
Corey Cleary Author

Haha yeah this one's classic - good point too, you can hit "promise hell" too if you're not careful. Not as "pyramid" like as callback hell, but having ton's and tons of then's can get sort of annoying

Collapse
l2aelba profile image
l2aelba

Think also if it's also with If-Else Statements :)

Collapse
ogrotten profile image
ogrotten

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.

I can appreciate trying to dispense some knowledge, but why not use one of those "real-world" cases in the example instead of the highly abstract setTimeout()?

I've been trying to grasp promises and async/await for "a while" now, but examples and articles I find like this, as you've said, use the wholly unrealistic setTimeout().

I completed a personal project a few months ago in NodeJS that was pretty standard AJAX activity . . .

  1. Get data from front end.
  2. Build it to JSON.
  3. Check if a file exists.
  4. If not, open a new file.
  5. If ok, write the file.
  6. If ok, close the file.
  7. If ok, tell the front end to show the new entry.

It took finishing it to realize that I built a several indention deep callback hell. Great. Well at least now I have a base to find examples for promises.

Not. Every example on the first page of my various google searches all used setTimeout().

I'd love to see an example of callback > promise > async/await based on, as you've said, realistic file, database or CORS calls.

Collapse
rifi2k profile image
Reilly Lowery

Real example and also I would simplify the error handling a ton and lose the try catch.

/**
 * Async Await Error Handling
 *
 * Example:
 *  const [err, response] = await to(
 *    axios.get(url)
 *  );
 * if (err) throw new Error('u broke it');
 * const hi = response.hi;
 *
 * @module to
 */

export default (promise) =>
  promise.then((data) => [null, data]).catch((err) => [err]);

You can pass anything that returns a promise to that module like so.

So here we are using the WP backbone api to return a collection of whatever and checking if more exist each time, when its all done we are combining them all together and loading the result into the data variable so we can stop our loading event on the table and show all the results. The js api will only return 100 items in a collection at a time and in this case we needed to grab 200-300 of whatever and load the table with all the data so the searching and sorting is instant so we sacrifice the inital time to grab all the data from better UI once we do.

PS: This is the basic layout of a Vue component, I just left off the methods wrapper around the loadItems function so you could see it easier.

import to from './to';

export default {

  data() {
    return {
      tableLoading: false,
      allTasks: []
    };
  },

  async loadItems() {

    // Start loading.
    this.tableLoading = true;

    // All our things will be put here.
    let all = [];

    // Get the collection of tasks.
    const tasksCollection = new wp.api.collections.Task();

    // Now we can use our to module for error handling.
    // collection.fetch could be axios or whatever returning a promise.
    const [err, response] = await to(
      tasksCollection.fetch({
        data: { per_page: 100 }
      })
    );
    if (err) throw new Error('Failed to load task collection');

    // We have lodashES in our app already so union() is available here.
    all = union(all, response);

    // This is checking if more exist, but it could be anything
    // your flow was dependant on.
    const hasMore = () => tasksCollection.hasMore();

    // Looping through another chunk.
    const loopMore = async () => {
      if (hasMore()) {
        const [errMore, responseMore] = await to(tasksCollection.more());
        if (errMore) throw new Error('Failed to load task collection');
        all = union(all, responseMore);
        loopMore();
      } else {
        // No more we are done, up yonder in the file we are
        // waiting for allTasks to be set to do other things.
        this.allTasks = all;
      }
    };

    loopMore();
  },

}

With async await and your example you described you would basically have a method or function returning a promise for checking on if the file exists and reading the data from it, probably using axios or fetch. Then another method returning a promise to create and write to a file. Each method would return a response and you would await on the result before doing the next part.

Hope this gave you a real world example to see how this stuff can be used.

Collapse
ccleary00 profile image
Corey Cleary Author

Yeah I've got another post in my backlog that will be something more along the lines of, finding some open source code snippet written using callbacks and refactoring the whole thing to show the before and after.

This post is really meant more to show the process, and I didn't want to overwhelm with too much information. However I understand some people learn better if they see something taken from an existing application and rewritten.

Thanks for the comment

Collapse
marneborn profile image
Mikael Arneborn

Use bluebirds .from callback function to easily to await on a function that takes a callback).

This only works if your callbacks are called in the standard way though, ie (err, result).

const Promise = require('bluebird');

await Promise.fromCallback(callback => functionThatTakesACallback(arg1, arg2, callback));

Collapse
ns23 profile image
Nitesh Sawant

We can also use util.promisify() to convert callback to promise

Collapse
zeropaper profile image