DEV Community

Cover image for Asynchronous JavaScript—How Callbacks, Promises, and Async-Await Work
Nick Scialli (he/him)
Nick Scialli (he/him)

Posted on

Asynchronous JavaScript—How Callbacks, Promises, and Async-Await Work

Please give this post a 💓, 🦄, or 🔖 if it you enjoyed it!

JavaScript touts asynchronous programming as a feature. This means that, if any action takes a while, your program can continue doing other things while the action completes. Once that action is done, you can do something with the result. This turns out the be a great feature for functionality like data fetching, but it can be confusing to newcomers. In JavaScript, we have a few different ways to handle asynchronicity: callback functions, Promises, and async-await.


I make other easy-to-digest tutorial content! Please consider:


Callback Functions

A callback function is a function you provide that will be executed after completion of the async operation. Let’s create a fake user data fetcher and use a callback function to do something with the result.

The Fake Data Fetcher

First we create a fake data fetcher that doesn’t take a callback function. Since fakeData doesn’t exist for 300 milliseconds, we don’t have synchronous access to it.

const fetchData = userId => {
  setTimeout(() => {
    const fakeData = {
      id: userId,
      name: 'George',
    };
    // Our data fetch resolves
    // After 300ms. Now what?
  }, 300);
};
Enter fullscreen mode Exit fullscreen mode

In order to be able to actually do something with our fakeData, we can pass fetchData a reference to a function that will handle our data!

const fetchData = (userId, callback) => {
  setTimeout(() => {
    const fakeData = {
      id: userId,
      name: 'George',
    };
    callback(fakeData);
  }, 300);
};
Enter fullscreen mode Exit fullscreen mode

Let’s create a basic callback function and test it out:

const cb = data => {
  console.log("Here's your data:", data);
};

fetchData(5, cb);
Enter fullscreen mode Exit fullscreen mode

After 300ms, we should see the following logged:

Here's your data: {id: 5, name: "George"}
Enter fullscreen mode Exit fullscreen mode

Promises

The Promise object represents the eventual completion of an operation in JavaScript. Promises can either resolve or reject. When a Promise resolves, you can handle its returned value with then then method. If a Promise is rejected, you can use the catch the error and handle it.

The syntax of the Promise object is as follows:

new Promise(fn);
Enter fullscreen mode Exit fullscreen mode

Were fn is a function that takes a resolve function and, optionally, a reject function.

fn = (resolve, reject) => {};
Enter fullscreen mode Exit fullscreen mode

The Fake Data Fetcher (with Promises)

Let’s use the same fake data fetcher as before. Instead of passing a callback, we’re going to return a new Promise object the resolves with our user’s data after 300ms. As a bonus, we can give it a small chance of rejecting as well.

const fetchData = userId => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() < 0.1) {
        reject('Fetch failed!');
      }
      const fakeData = {
        id: userId,
        name: 'George',
      };
      resolve(fakeData);
    }, 300);
  });
};
Enter fullscreen mode Exit fullscreen mode

Our new fetchData function can be used as follows:

fetchData(5)
  .then(user => {
    console.log("Here's your data:", user);
  })
  .catch(err => {
    console.error(err);
  });
Enter fullscreen mode Exit fullscreen mode

If fetchData successfully resolves (this will happen 90% of the time), we will log our user data as we did with the callback solution. If it is rejected, then we will console.error the error message that we created (“Fetch failed!“)

One nice thing about Promises is you can chain then to execute subsequent Promises. For example, we could do something like this:

fetchData(5)
  .then(user => {
    return someOtherPromise(user);
  })
  .then(data => {
    console.log(data);
  })
  .catch(err => {
    console.error(err);
  });
Enter fullscreen mode Exit fullscreen mode

Furthermore, we can pass an array of Promises to Promise.all to only take action after all Promises are resolved:

Promise.all([fetchData(5), fetchData(10)])
  .then(users => {
    console.log("Here's your data:", users);
  })
  .catch(err => {
    console.error(err);
  });
Enter fullscreen mode Exit fullscreen mode

In this case, if both Promises are successfully resolved, the following will get logged:

Here's your data:
[{ id: 5, name: "George" }, { id: 10, name: "George" }]
Enter fullscreen mode Exit fullscreen mode

Async-Await

Async-await offers a different syntax for writing Promises that some find clearer. With async-await, you can create an async function. Within that async function, you can await the result of a Promise before executing subsequent code! Let’s look at our data fetch example.

const fetchUser = async userId => {
  const user = await fetchData(userId);
  console.log("Here's your data:", user);
};
fetchUser(5);
Enter fullscreen mode Exit fullscreen mode

Pretty nice, right? One small wrinkle: we’re not handling our Promise rejection case. We can do this with try/catch.

const fetchUser = async userId => {
  try {
    const user = await fetchData(userId);
    console.log("Here's your data:", user);
  } catch (err) {
    console.error(err);
  }
};
fetchUser(5);
Enter fullscreen mode Exit fullscreen mode

Browser/Node Support

Since callback functions are just normal functions being passed to other functions, there’s no concern about support. Promises have been standard since ECMAScript 2015 and have decent support, but are not supported in Internet Explorer. Async-await is newer (standard since ECMAScript 2017) and is has good support in newer browser versions. Again, it isn’t supported in Internet Exporer.

On the node side, async-await (and therefore, Promises) have been well-supported since nove v7.6.

Top comments (10)

Collapse
 
ziizium profile image
Habdul Hazeez

Possible typographical errors:

Under the section "Promises" you wrote:

When a Promise resolves, you can handle its returned value with then then method

In the last paragraph you wrote:

On the node side, async-await (and therefore, Promises) have been well-supported since nove v7.6.

Collapse
 
michaelcurrin profile image
Michael Currin • Edited

Thanks for sharing. Nice comparison of the three approaches. I needed to see something like this.

Though, I struggled to see how async and await syntax works compared with promises syntax. I see now how async replaces resolve and await replaces .then - this guide explains it well:

javascript.info/async-await

Also it defines functions using traditional syntax rather than const and arrow functions so it was easier to read to learn async syntax.

Collapse
 
prafulla-codes profile image
Prafulla Raichurkar

Thank you for the clean explanation, I'm surely going to refactor my code with promises. I did not know that promises can be chained :)

Collapse
 
degesis profile image
PovilasB

This is the best explanation of promises, callback,async/await i have read so far.

Collapse
 
nas5w profile image
Nick Scialli (he/him)

Thank you!!

Collapse
 
adrianghub profile image
Adrian Zinko

Great article! I've been using async/await syntax ever since and now I see how valuable understanding of that can be for productive workflows.

Collapse
 
mouseannoying profile image
Dominic Myers

Excellent and clear explanation! Bravo!

Collapse
 
cycostallion profile image
CycoStallion

A very clean and crisp example to explain the concepts. Loved it!

Collapse
 
jarvis3000 profile image
Jarvis-3000

Astounding post
Gained a lot of knowledge with deleting many confusions. 👌

Collapse
 
nativeforest profile image
nativeforest

Why some people use async/await and promises at the same time, for example in Axios(who return a promise) using async with try catch