Understanding Callbacks, Promises, and Async/Await in JavaScript

Table of contents

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
Feature | Callbacks | Promises | Async/Await |
Readability | Difficult (callback hell). | Moderate (chaining). | Easy (clean syntax). |
Error Handling | Needs manual handling. | .catch() for errors. | try/catch blocks. |
Syntax | Nested functions. | Chainable methods. | Looks synchronous. |
Performance | Works for small tasks. | Better control. | Best with Promises. |
When to Use Each?
Callbacks: For simple operations (e.g.,
setTimeout
, event listeners).Promises: When you need better error handling and chaining.
Async/Await: When writing complex asynchronous logic to keep the code readable.
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.