Understanding Callbacks, Promises, and Async/Await in JavaScript

Master Callbacks, Promises, and Async/Await in JavaScript with This Guide

JavaScript provides multiple ways to handle asynchronous operations. Let's delve into callbacks, Promises, and async/await with detailed explanations and examples.


1. Callbacks

A callback is a function passed as an argument to another function, which is then executed after the completion of an asynchronous operation.

Key Points:

  • A traditional way to handle asynchronous code.

  • May lead to "callback hell" if not managed well.

Example 1: Basic Callback

function fetchData(callback) {
  setTimeout(() => {
    console.log("Data fetched.");
    callback();
  }, 2000);
}

fetchData(() => {
  console.log("Callback executed.");
});
// Output:
// Data fetched.
// Callback executed.

Example 2: Nested Callbacks (Callback Hell)

function fetchData(callback) {
  setTimeout(() => {
    console.log("Step 1: Data fetched.");
    callback();
  }, 1000);
}

fetchData(() => {
  setTimeout(() => {
    console.log("Step 2: Data processed.");
    setTimeout(() => {
      console.log("Step 3: Data saved.");
    }, 1000);
  }, 1000);
});
// Output:
// Step 1: Data fetched.
// Step 2: Data processed.
// Step 3: Data saved.

2. Promises

A Promise is an object representing the eventual completion or failure of an asynchronous operation. It provides better readability and avoids "callback hell."

Key Points:

  • Has three states: pending, fulfilled, rejected.

  • Methods:

    • .then() for handling success.

    • .catch() for handling errors.

    • .finally() for cleanup operations.

Example 1: Creating and Using Promises

const fetchData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let success = true;
      if (success) resolve("Data fetched successfully!");
      else reject("Error fetching data.");
    }, 2000);
  });
};

fetchData()
  .then((message) => {
    console.log(message); // Output: Data fetched successfully!
  })
  .catch((error) => {
    console.error(error);
  });

Example 2: Chaining Promises

const fetchData = () => {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Step 1: Data fetched."), 1000);
  });
};

fetchData()
  .then((message) => {
    console.log(message);
    return new Promise((resolve) =>
      setTimeout(() => resolve("Step 2: Data processed."), 1000)
    );
  })
  .then((message) => {
    console.log(message);
    return new Promise((resolve) =>
      setTimeout(() => resolve("Step 3: Data saved."), 1000)
    );
  })
  .then((message) => {
    console.log(message);
  });
// Output:
// Step 1: Data fetched.
// Step 2: Data processed.
// Step 3: Data saved.

3. Async/Await

async/await is a modern syntax built on top of Promises, making asynchronous code look and behave like synchronous code.

Key Points:

  • Functions using async automatically return a Promise.

  • await pauses the execution until the Promise is resolved or rejected.

  • Provides cleaner and more readable code.

Example 1: Basic Async/Await

const fetchData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let success = true;
      if (success) resolve("Data fetched successfully!");
      else reject("Error fetching data.");
    }, 2000);
  });
};

const handleData = async () => {
  try {
    const message = await fetchData();
    console.log(message); // Output: Data fetched successfully!
  } catch (error) {
    console.error(error);
  }
};

handleData();

Example 2: Sequential Async/Await

const fetchStep1 = () => new Promise((resolve) => setTimeout(() => resolve("Step 1 complete."), 1000));
const fetchStep2 = () => new Promise((resolve) => setTimeout(() => resolve("Step 2 complete."), 1000));
const fetchStep3 = () => new Promise((resolve) => setTimeout(() => resolve("Step 3 complete."), 1000));

const processSteps = async () => {
  console.log(await fetchStep1());
  console.log(await fetchStep2());
  console.log(await fetchStep3());
};

processSteps();
// Output:
// Step 1 complete.
// Step 2 complete.
// Step 3 complete.

Example 3: Parallel Async/Await

const fetchStep1 = () => new Promise((resolve) => setTimeout(() => resolve("Step 1 complete."), 1000));
const fetchStep2 = () => new Promise((resolve) => setTimeout(() => resolve("Step 2 complete."), 1000));
const fetchStep3 = () => new Promise((resolve) => setTimeout(() => resolve("Step 3 complete."), 1000));

const processSteps = async () => {
  const results = await Promise.all([fetchStep1(), fetchStep2(), fetchStep3()]);
  console.log(results); // Output: [ 'Step 1 complete.', 'Step 2 complete.', 'Step 3 complete.' ]
};

processSteps();

Comparison

FeatureCallbacksPromisesAsync/Await
ReadabilityDifficult (callback hell).Moderate (chaining).Easy (clean syntax).
Error HandlingNeeds manual handling..catch() for errors.try/catch blocks.
SyntaxNested functions.Chainable methods.Looks synchronous.
PerformanceWorks for small tasks.Better control.Best with Promises.

When to Use Each?

  1. Callbacks: For simple operations (e.g., setTimeout, event listeners).

  2. Promises: When you need better error handling and chaining.

  3. Async/Await: When writing complex asynchronous logic to keep the code readable.

Did you find this article valuable?

Support CodeWords by becoming a sponsor. Any amount is appreciated!