Understanding Callbacks, Promises, and Async/Await in JavaScript

IndrajeetIndrajeet
4 min read

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.

0
Subscribe to my newsletter

Read articles from Indrajeet directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Indrajeet
Indrajeet

Hello there! My name is Indrajeet, and I am a skilled web developer passionate about creating innovative, user-friendly, and dynamic web applications. 📌My Expertise advanced proficiency in angular front-End Development. Back-End Development Leveraging the power of .NET, I build secure, robust, and scalable server-side architectures.