Callbacks, Promises, and async/await

These are three ways to handle work that finishes later, such as loading data from a server, reading a file, or waiting for a timer.

The problem they solve

Some operations do not produce a result immediately. A network request might take 200 milliseconds or several seconds.

JavaScript starts the operation and continues running other code. You need a way to say, “When the result is ready, run this code.”

This code cannot use the user until the request finishes:
const user = getUser(42);
console.log(user.name);

Callbacks, Promises, and async/await provide different ways to wait for that result.

Callbacks

A callback is a function passed to another function. The other function calls it when the work is complete.

Pass the next step as a function:
getUser(42, (error, user) => {
  if (error) {
    console.error(error);
    return;
  }

  console.log(user.name);
});
  1. getUser starts the request.
  2. JavaScript continues running other code.
  3. The request finishes.
  4. getUser calls the callback with an error or a user.

A callback is just a function. Callbacks can also be used for synchronous operations, but they are common in asynchronous APIs.

Several dependent callbacks create nesting:
getUser(42, (error, user) => {
  if (error) return handleError(error);

  getPosts(user.id, (error, posts) => {
    if (error) return handleError(error);
    showPosts(posts);
  });
});

The repeated error checks and increasing indentation become difficult to manage as more steps are added.

Promises

A Promise is an object that represents one result that may become available later.

A Promise is always in one of three states:

  • Pending: the operation is still running.
  • Fulfilled: the operation completed successfully and produced a value.
  • Rejected: the operation failed and produced an error.

.then() handles a fulfilled value. .catch() handles a rejection.

The same user request with a Promise:
getUser(42)
  .then(user => {
    console.log(user.name);
  })
  .catch(error => {
    console.error(error);
  });

Promises are useful because they can be chained. Each .then() produces a new Promise.

Return the next Promise so the chain waits for it:
getUser(42)
  .then(user => {
    return getPosts(user.id);
  })
  .then(posts => {
    showPosts(posts);
  })
  .catch(handleError);

If you forget to return getPosts(), the next .then() will run without waiting for it.

Async and await

Async/await is another way to work with Promises. It does not replace Promises.

An async function always returns a Promise. Inside that function, await waits for a Promise to settle.

The same request written with async/await:
async function showUser() {
  try {
    const user = await getUser(42);
    console.log(user.name);
  } catch (error) {
    console.error(error);
  }
}

showUser();
  1. getUser(42) returns a Promise.
  2. await pauses showUser until that Promise settles.
  3. If it fulfills, user receives the value.
  4. If it rejects, execution moves to catch.

await pauses only the current async function. It does not block the entire JavaScript program.

Dependent steps read from top to bottom:
async function loadPosts() {
  try {
    const user = await getUser(42);
    const posts = await getPosts(user.id);
    showPosts(posts);
  } catch (error) {
    handleError(error);
  }
}

Waiting in sequence or together

Use consecutive awaits when the second operation needs the first result.

These requests are dependent:
const user = await getUser(42);
const posts = await getPosts(user.id);

If operations do not depend on each other, start them together with Promise.all().

These requests are independent:
const [user, settings] = await Promise.all([
  getUser(42),
  getSettings()
]);

Consecutive awaits

The second operation starts after the first finishes.

Promise.all

Both operations start immediately, then the code waits for both.

How the three styles compare

Style Success Failure
Callback Callback argument Error argument
Promise .then() .catch()
Async/await await try/catch

Practical rules

  1. Use async/await for most Promise-based application code.

    It usually makes dependent steps easier to read.

  2. Use callbacks for events and callback-only APIs.

    For example, click handlers and older Node.js APIs.

  3. Return or await every Promise that matters.

    This keeps errors and completion connected to the caller.

  4. Use Promise.all for independent operations.

    This avoids waiting longer than necessary.

Check your understanding

  1. What does a callback provide to an asynchronous function?

    Show answer

    It provides the code to run after the operation finishes. The asynchronous function calls the callback with the result or an error.

  2. What are the three states of a Promise?

    Show answer

    Pending means the operation has not finished. Fulfilled means it completed successfully and produced a value. Rejected means it failed and produced an error.

  3. Does await block the entire JavaScript program?

    Show answer

    No. It pauses only the current async function. Other JavaScript can continue running while the function waits for the Promise.

  4. When should you use Promise.all() instead of consecutive awaits?

    Show answer

    Use it when the operations are independent. They can start together, which usually finishes sooner than waiting for each one separately.

  5. What does this code print?

    Promise.resolve(2)
      .then(number => number * 3)
      .then(console.log);
    Show answer

    It prints 6. The first .then() receives 2 and returns 6. The next .then() receives that returned value.

  6. In what order does this code print the letters?

    async function run() {
      console.log("A");
      await Promise.resolve();
      console.log("B");
    }
    
    run();
    console.log("C");
    Show answer

    It prints A, C, then B. Calling run() prints A. The await pauses run, so the program continues and prints C. Then run resumes and prints B.

  7. What does this code print?

    async function run() {
      try {
        await Promise.reject("Failed");
        console.log("Done");
      } catch (error) {
        console.log(error);
      }
    }
    
    run();
    Show answer

    It prints Failed. Awaiting the rejected Promise moves execution directly to catch, so Done is never printed.