From Callbacks → Callback Hell → Promises → Async/Await (JavaScript)

I'm Shubham (@shubhamsinghbundela), I'm a Software Engineer, a Full-stack developer, a tech enthusiast, and a technical writer here on @Hashnode. I have a strong zeal to share my acquired knowledge and I am also willing to learn from others.
When I started learning async JavaScript, I was confused about how things evolved from callbacks to async/await.
So here’s a simple breakdown with examples
1. What is a Callback?
A callback is just a function passed into another function and executed later.
Example
function xyz(text, cb) {
cb(text);
}
xyz("shubham", (data) => {
console.log(data);
});
Output:
shubham
Making it Asynchronous (using setTimeout)
function xyz(text, cb) {
setTimeout(() => {
cb(text);
}, 1000);
}
xyz("shubham", (data) => {
console.log(data);
});
Output (after 1 sec):
shubham
2. Callback Hell
When multiple async tasks depend on each other, callbacks get nested.
Example
function xyz(text, cb) {
setTimeout(() => {
cb(text);
}, 1000);
}
xyz("step 1", (data1) => {
console.log(data1);
xyz("step 2", (data2) => {
console.log(data2);
xyz("step 3", (data3) => {
console.log(data3);
xyz("step 4", (data4) => {
console.log(data4);
});
});
});
});
Problems:
Hard to read
Hard to debug
Why Promises Came
To solve callback hell, JavaScript introduced Promises.
3. Promises Version (Clean)
function xyz(text) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(text);
}, 1000);
});
}
xyz("step 1")
.then((data1) => {
console.log(data1);
return xyz("step 2");
})
.then((data2) => {
console.log(data2);
return xyz("step 3");
})
.then((data3) => {
console.log(data3);
return xyz("step 4");
})
.then((data4) => {
console.log(data4);
})
.catch(console.error);
Benefits:
No nesting
Better readability
Proper error handling
4. Async/Await Version (Best & Cleanest)
Async/Await makes async code look like normal synchronous code.
function xyz(text) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(text);
}, 1000);
});
}
async function run() {
try {
const data1 = await xyz("step 1");
console.log(data1);
const data2 = await xyz("step 2");
console.log(data2);
const data3 = await xyz("step 3");
console.log(data3);
const data4 = await xyz("step 4");
console.log(data4);
} catch (err) {
console.error(err);
}
}
run();
Benefits:
Looks synchronous
Easy to read
Cleaner error handling
5. await vs .then()
Equivalent Code
const data = await xyz("step 1");
console.log(data);
Same as:
xyz("step 1").then((data) => {
console.log(data);
});
await is just syntactic sugar over Promises (.then), making async code look synchronous.
Final Summary
Callback → Nested (Callback Hell)
Promise → Flat chaining
Async/Await → Clean & readable




