Callbacks, Promises & Async – await:
Callbacks -> Callback Hell -> Promises -> Promise Chaining -> Async/Await -> IIFE
Synchronous vs Asynchronous
Synchronous
The code that runs line by line is called a synchronous code. Each instruction waits for the previous one to finish before moving on to the next.But the problem is it can block the UI if a task is slow.
Asynchronous
The code that takes some time to get executed or slow tasks (timers, API calls) are handed off. If there are multiple lines of code before and after the asynchronous code then remaining code keeps running and it does not wait for the asynchronous code to get executed. Result is handled later when ready.
// Async demo with setTimeout
console.log("one");
console.log("two");
setTimeout(() => {
console.log("hello"); // runs after 4s
}, 4000);
console.log("three");
console.log("four");
// Output: one → two → three → four → hello
Callbacks
A callback is a function passed as an argument to another function and called inside it. It's how JS handles "do this first, then do that" logic.
function sum(a, b) {
console.log(a + b);
}
function calculator(a, b, sumCallback) {
sumCallback(a, b); // sum is called here
}
calculator(1, 2, sum); // Output: 3
Callback Hell (Pyramid of Doom)
When callbacks are nested inside each other to sequence async tasks, code becomes deeply indented and hard to read and maintain. This is called Callback Hell.
eg:
function getData(dataId, getNextData) {
setTimeout(() => {
console.log("data", dataId);
if (getNextData) getNextData();
}, 2000);
}
// Callback Hell
getData(1, () => {
console.log("getting data2...");
getData(2, () => {
console.log("getting data3...");
getData(3, () => {
console.log("getting data4...");
getData(4);
});
});
});
The deeper the nesting, the harder to read, debug, and maintain. Promises solve this issue.
Promises
A Promise is a JS object representing the eventual completion or failure of an asynchronous task. It takes a function with two handlers: resolve (success) and reject (failure).
There are 3 states of a promise, they are
i.Pending
Result is undefined. Task still in progress.
ii.Fulfilled
resolve(value) was called. Task succeeded.
iii.Rejected
reject(error) was called. Task failed.
// Creating a Promise
const getPromise = () => {
return new Promise((resolve, reject) => {
console.log("I am a promise");
resolve("success"); // OR
// reject("error");
});
};
// Consuming a Promise
let promise = getPromise();
promise.then((res) =>{
console.log("Fulfilled:", res);//runs if promise gets resolved or fullfilled
});
promise.catch((err) => {
console.log("Rejected:", err); // runs if promise gets rejected
});
resolve and reject are callbacks provided by JS -- you just call them. If you reject without .catch(), you get an Uncaught (in promise) error in console.
Promise Chaining
Instead of nesting, return a new Promise inside .then() and chain another .then(). This keeps code flat and readable -- this is the fix for callback hell.
eg:
function getData(dataId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("data", dataId);
resolve("success");
}, 3000);
});
}
// Promise Chain (clean & flat!)
getData(1)
.then((res) => { return getData(2); })
.then((res) => { return getData(3); })
.then((res) => { console.log(res); });
Async / Await
When we use async before a function makes it always return a Promise.
await pauses execution inside the async function until a Promise settles -- making async code look and read like synchronous code.
function getData(dataId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("data", dataId);
resolve("success");
}, 2000);
});
}
// Async-Await
async function getAllData() {
console.log("getting data1...");
await getData(1); // waits here
console.log("getting data2...");
await getData(2); // then waits here
console.log("getting data3...");
await getData(3);
}
getAllData();
Still in async await we always have to keep the await data inside a function and every time call it to show the execution..to solve this issue we have IIFE...
IIFE — Immediately Invoked Function Expression
An IIFE is a function that runs immediately as soon as it is defined. It only executes once. Useful to run async code at the top level without wrapping it in a named function.
// Regular IIFE
(function () {
// runs immediately
})();
// Arrow IIFE
(() => {
// runs immediately
})();
// Async IIFE — most common use case
(async () => {
console.log("getting data1...");
await getData(1);
console.log("getting data2...");
await getData(2);
})();
*Pattern: *(func)() — wrap function in () to make it an expression, then call it with () immediately.
Summary
Callbacks → Callback Hell problem → Promises fix it → Promise Chaining improves readability → Async/Await makes it look synchronous → IIFE lets you run async code immediately.
Top comments (0)