Asynchronous programming is one of the most crucial concepts in JavaScript, especially for frontend developers. Whether you're fetching data, handling events, or executing delayed operations, understanding async programming helps you write cleaner and more efficient code. In this blog, we'll explore three key approaches to handling asynchronous code in JavaScript: callbacks, promises, and async/await. We'll also discuss common pitfalls like callback hell and how modern techniques make async programming easier to manage.
Understanding callbacks
A callback is simply a function passed as an argument to another function, which is then executed later. Callbacks are fundamental in JavaScript for handling asynchronous tasks, event listeners, and functional programming.
Basic callback example
function saveOrder(orderId, userEmail, callback) {
const order = { orderId, userEmail };
console.log(order);
callback(userEmail); // Execute the callback function
}
function sendOrderEmail(userEmail) {
console.log(`Order confirmed for ${userEmail}`);
}
saveOrder("145908275", "user@gmail.com", sendOrderEmail);
Asynchronous callback example
console.log("JavaScript is awesome");
setTimeout(() => {
console.log("This code runs after 2 seconds");
}, 2000);
console.log("Scribbler is awesome");
Output:
JavaScript is awesome
Scribbler is awesome
This code runs after 2 seconds
Callback hell
When callbacks are nested too deeply, they become difficult to read and maintain.
function saveOrder(orderId, userEmail, callback) {
const order = { orderId, userEmail };
console.log(order);
callback(userEmail);
}
function sendDetailsToSeller(orderId, userEmail, callback) {
const details = { orderId, userEmail };
console.log(details);
callback(userEmail);
}
function sendOrderEmail(userEmail) {
console.log(`Order confirmed for ${userEmail}`);
}
saveOrder("145908275", "user@gmail.com", () =>
sendDetailsToSeller("145908275", "user@gmail.com", sendOrderEmail)
);
Problem: Nested callbacks make the code harder to read and debug.
Solution: Use Promises or Async/Await.
Promises
A Promise represents the eventual success or failure of an asynchronous operation. It helps avoid callback hell by allowing us to chain operations more clearly.
Basic promise example
const basicPromise = new Promise((resolve, reject) => {
setTimeout(() => {
let success = true;
success ? resolve("Task completed successfully! ✅") : reject("Task failed ❌");
}, 2000);
});
basicPromise
.then((result) => console.log(result))
.catch((error) => console.log(error))
.finally(() => console.log("Promise execution completed."));
Fetching data with promises
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.log("Fetch Error:", error));
Promise chaining
new Promise((resolve) => {
setTimeout(() => resolve(10), 1000);
})
.then(num => {
console.log(num); // 10
return num * 2;
})
.then(num => {
console.log(num); // 20
return num * 3;
})
.then(num => console.log(num)); // 60
Promise methods
Promise.all - Resolves when all promises are resolved (fails if any reject).
Promise.race - Resolves when the first promise resolves or rejects.
Promise.allSettled - Waits for all promises to complete and returns results.
Promise.any - Resolves with the first fulfilled promise (ignores failures).
const promise1 = new Promise(resolve => setTimeout(() => resolve(3), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve(4), 1000));
const promise3 = new Promise(resolve => setTimeout(() => resolve(5), 1000));
Promise.all([promise1, promise2, promise3]).then(console.log); // [3, 4, 5]
Promise.race([promise1, promise2, promise3]).then(console.log); // First resolved value
Promise.allSettled([promise1, promise2, promise3]).then(console.log);
Promise.any([promise1, promise2, promise3]).then(console.log);
Async/Await
Async/Await simplifies working with promises by allowing us to write asynchronous code in a synchronous-like manner.
Why use Async/Await?
More readable than chaining
.then()
Handles errors better with
try/catch
Easier to debug
Solving callback hell with Async/Await
async function saveOrder(orderId, userEmail) {
const order = { orderId, userEmail };
console.log(order);
return order;
}
async function sendDetailsToSeller(orderId, userEmail) {
const details = { orderId, userEmail };
console.log(details);
return details;
}
async function sendOrderEmail(userEmail) {
console.log(`Order confirmed for ${userEmail}`);
return "Email sent";
}
async function processOrder(orderId, userEmail) {
await saveOrder(orderId, userEmail);
await sendDetailsToSeller(orderId, userEmail);
await sendOrderEmail(userEmail);
}
processOrder("145908275", "user@gmail.com");
Fetching data with Async/Await
async function fetchData() {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return await response.json();
}
fetchData()
.then(data => console.log("Fetched Data:", data))
.catch(error => console.log("Error:", error));
Async/Await with loops
async function fetchTodos() {
const urls = [
"https://jsonplaceholder.typicode.com/todos/1",
"https://jsonplaceholder.typicode.com/todos/2",
"https://jsonplaceholder.typicode.com/todos/3"
];
for (let url of urls) {
let response = await fetch(url);
let data = await response.json();
console.log(data);
}
}
fetchTodos();
Conclusion
By understanding callbacks, promises, and async/await, you can write asynchronous JavaScript code that is clean, readable, and efficient. Async/Await is the recommended approach in modern JavaScript, but knowing Promises and Callbacks helps you work with legacy code and APIs.