Promises — Escaping Callback Hell and Managing Async Operations Like a Professional
The Day JavaScript Code Started Looking Like a Pyramid
A few years ago, when I was building a small application, I had a simple requirement.
The application needed to:
Login the user
Fetch user details
Fetch user orders
Generate a report
Seems straightforward.
As a beginner, I wrote something like this:
loginUser(function(user) {
getUserDetails(user.id, function(details) {
getOrders(details.id, function(orders) {
generateReport(orders, function(report) {
console.log(report);
});
});
});
});
The code worked.
But after looking at it for a few minutes, I realized something.
The code was becoming harder to read.
Harder to maintain.
Harder to debug.
Every new step pushed the code further to the right.
It looked like a staircase.
Or worse...
A pyramid.
Developers gave this problem a famous name:
Callback Hell
And Promises were introduced to solve it.
Before Promises Existed
Let's quickly revisit how asynchronous code was originally written.
Example:
setTimeout(() => {
console.log("Task Complete");
}, 2000);
This is called a callback.
A callback is simply:
A function passed into another function
to be executed later.
Nothing wrong with callbacks.
The problem appears when multiple async operations depend on each other.
Real World Example
Imagine an AQAD retailer places an order.
The system needs to:
Validate User
↓
Check Inventory
↓
Calculate Price
↓
Process Payment
↓
Generate Invoice
Using nested callbacks:
validateUser(function(user) {
checkInventory(function(stock) {
calculatePrice(function(price) {
processPayment(function(payment) {
generateInvoice();
});
});
});
});
The code starts drifting right.
Adding more steps makes it even worse.
Why Callback Hell Is Dangerous
The problem isn't just ugly code.
Callback Hell causes:
Reduced Readability
The code becomes difficult to understand.
Difficult Debugging
Finding errors becomes harder.
Hard Maintenance
Future developers struggle to modify the code.
Increased Bugs
Nested logic often creates mistakes.
This is why JavaScript developers needed a better solution.
Enter Promises
Think of a Promise like a restaurant order token.
Imagine you order food.
The cashier doesn't immediately hand you the meal.
Instead, they give you a token.
The token represents a promise.
It says:
Your food is being prepared.
You don't know exactly when it will be ready.
But you know one of two things will happen.
Either:
Food Ready
or
Order Failed
Promises work exactly like this.
What Is a Promise?
A Promise is an object that represents the eventual completion or failure of an asynchronous operation.
Don't worry about memorizing that definition.
Think about it like this:
A Promise is a placeholder
for a future result.
The result may arrive later.
But JavaScript gives us a structured way to handle it.
Promise States
Every Promise has a state.
There are three possible states.
1. Pending
The task is still running.
Example:
Food Being Prepared
2. Fulfilled
The task completed successfully.
Example:
Food Delivered
3. Rejected
The task failed.
Example:
Restaurant Out of Ingredients
Visualization:
Pending
|
+---- Fulfilled
|
+---- Rejected
A Promise starts as Pending.
Then becomes either:
Fulfilled
or
Rejected
Never both.
Creating Your First Promise
Example:
const orderFood = new Promise((resolve, reject) => {
let restaurantOpen = true;
if (restaurantOpen) {
resolve("Burger Ready");
} else {
reject("Restaurant Closed");
}
});
Here:
resolve()
means success.
And:
reject()
means failure.
Consuming a Promise
Creating a Promise is only half the story.
We must also handle its result.
Example:
orderFood.then((result) => {
console.log(result);
});
Output:
Burger Ready
The keyword:
then()
runs when the Promise succeeds.
Understanding then()
Think about your food order token.
When food becomes available:
Then give me the burger.
That is exactly what:
.then()
means.
Handling Failures with catch()
Not every operation succeeds.
Example:
const orderFood = new Promise((resolve, reject) => {
reject("Kitchen Closed");
});
Handling error:
orderFood
.then((result) => {
console.log(result);
})
.catch((error) => {
console.log(error);
});
Output:
Kitchen Closed
Why catch() Matters
Imagine processing a payment.
Things can fail.
Examples:
Network Error
Insufficient Funds
Server Down
Invalid Card
Without error handling:
Applications crash.
Professional developers always handle failures.
Promise Flow Visualization
Example:
fetchData()
Flow:
Pending
|
+---- Success → then()
|
+---- Failure → catch()
This structure makes code cleaner and easier to understand.
Simulating an API Request
Let's create a realistic example.
const fetchProducts = new Promise((resolve) => {
setTimeout(() => {
resolve("Products Loaded");
}, 2000);
});
Using:
fetchProducts.then((result) => {
console.log(result);
});
Output after 2 seconds:
Products Loaded
This is similar to what happens when a frontend requests data from a server.
Chaining Promises
One of the biggest advantages of Promises is chaining.
Example:
loginUser()
.then(getUserProfile)
.then(getOrders)
.then(generateInvoice)
.then(sendEmail);
Look at how clean that is.
Compare it with callback hell:
loginUser(function() {
getUserProfile(function() {
getOrders(function() {
generateInvoice(function() {
sendEmail();
});
});
});
});
The Promise version is much easier to read.
AQAD Marketplace Example
Imagine a retailer places an order.
Process:
Login
↓
Verify Account
↓
Check Stock
↓
Process Payment
↓
Generate Invoice
↓
Send Notification
Promise chain:
loginRetailer()
.then(verifyAccount)
.then(checkStock)
.then(processPayment)
.then(generateInvoice)
.then(sendNotification)
.catch(handleError);
This reads almost like English.
Returning Values in then()
Example:
Promise.resolve(10)
.then((value) => {
return value * 2;
})
.then((value) => {
console.log(value);
});
Output: 20
Each then() can pass data to the next one.
Think of it as a production line.
Each worker completes a task and passes the result forward.
Promise.resolve()
Creates an already successful Promise.
Example:
Promise.resolve("Success")
.then(console.log);
Output:
Success
Promise.reject()
Creates an already failed Promise.
Example:
Promise.reject("Error")
.catch(console.log);
Output:
Error
Promise.all()
Imagine ordering food for five friends.
You want everyone to receive food before starting dinner.
Example:
Promise.all([
fetchProducts(),
fetchCategories(),
fetchBrands()
])
.then((results) => {
console.log(results);
});
All tasks must succeed.
Only then does execution continue.
Real World Use Case
E-commerce homepage:
Products
Categories
Offers
Brands
All data can be loaded simultaneously.
This improves performance significantly.
Promise.race()
Imagine ordering from multiple delivery partners.
Whichever arrives first wins.
Example:
Promise.race([
serverOne(),
serverTwo()
]);
The fastest response is used.
Common Beginner Mistakes
Mistake 1
Forgetting catch()
Wrong:
fetchData()
.then(console.log);
Better:
fetchData()
.then(console.log)
.catch(console.error);
Mistake 2
Creating unnecessary Promises
Many beginners wrap everything inside Promises.
Not every operation needs one.
Use Promises primarily for asynchronous work.
Mistake 3
Not returning values
Wrong:
.then((value) => {
value * 2;
})
Correct:
.then((value) => {
return value * 2;
})
Why Promises Changed JavaScript
Before Promises:
Messy Nested Callbacks
After Promises:
Structured Async Flow
Developers could finally write asynchronous code in a cleaner way.
Applications became easier to maintain.
Teams became more productive.
Code reviews became simpler.
The Limitation of Promises
Even though Promises are much better than callbacks, they can still become verbose.
Example:
loginUser()
.then((user) => {
return getProfile(user);
})
.then((profile) => {
return getOrders(profile);
})
.then((orders) => {
return createInvoice(orders);
})
.catch((error) => {
console.log(error);
});
This is cleaner than callback hell.
But it can still feel repetitive.
JavaScript developers wanted something even better.
Something that looked more like normal code.
That solution became:
async
await
And it completely transformed how modern JavaScript is written.
Why Every Developer Must Understand Promises
Even if you use Async/Await every day, Promises still matter.
Because:
Async/Await is built on top of Promises.
Understanding Promises helps you understand:
API calls
Database queries
File operations
Authentication systems
Modern frontend frameworks
Modern backend applications
Promises are the foundation.
Async/Await is simply a more elegant way of using them.
Summary we learned:
What Callback Hell is
Why Promises were introduced
Promise states
Pending
Fulfilled
Rejected
resolve()
reject()
then()
catch()
Promise chaining
Promise.all()
Promise.race()
Common mistakes
Real-world applications
Think of a Promise like a restaurant order token.
You don't receive the result immediately.
Instead, you receive a guarantee that eventually one of two things will happen:
Success
or
Failure
Promises provide a clean and structured way to manage that uncertainty.
And now that we understand Promises, it's time to learn the feature that made asynchronous JavaScript feel almost synchronous.
A feature that most modern developers use every day.
Async Await — Writing Asynchronous Code That Feels Like Normal JavaScript
we'll learn why Async/Await was introduced, how it works behind the scenes, and why it became the preferred way to write asynchronous JavaScript.
After learning Promises, most developers feel relieved.
Finally:
loginUser()
.then(getProfile)
.then(getOrders)
.then(generateInvoice)
.catch(handleError);
looks much cleaner than Callback Hell.
But after working on larger projects, another problem appears.
Imagine you're building an e-commerce platform.
A retailer places an order.
The application needs to:
Validate User
↓
Fetch Cart
↓
Check Inventory
↓
Calculate Price
↓
Process Payment
↓
Generate Invoice
↓
Send Email
With Promise chains:
validateUser()
.then(fetchCart)
.then(checkInventory)
.then(calculatePrice)
.then(processPayment)
.then(generateInvoice)
.then(sendEmail)
.catch(handleError);
This isn't bad.
But when logic becomes more complex, chains become longer.
Conditions get added.
Loops appear.
Error handling expands.
Eventually developers started asking:
"Can asynchronous code look like normal JavaScript?"
The answer became:
async
await
And honestly, this changed the way developers write JavaScript.
A Simple Real-Life Analogy
Imagine you order food in a restaurant.
Without Async/Await:
Order Food
Then Wait
Then Receive Food
Then Eat
Then Pay
Everything is described as separate steps.
With Async/Await:
Order Food
Wait
Receive Food
Eat
Pay
The process feels more natural.
More readable.
Closer to how humans think.
That is exactly what Async/Await does.
Why Async Await Was Introduced
The purpose was simple:
Make asynchronous code easier to read and maintain.
Instead of:
fetchUser()
.then(user => getOrders(user))
.then(orders => createInvoice(orders))
.then(invoice => sendEmail(invoice))
.catch(error => console.log(error));
We can write:
const user = await fetchUser();
const orders = await getOrders(user);
const invoice = await createInvoice(orders);
await sendEmail(invoice);
Most developers find the second version easier to understand.
What Does async Mean?
The keyword:
async
is placed before a function.
Example:
async function greet() {
}
This tells JavaScript:
This function will work with Promises.
Think of it as informing JavaScript:
"Something asynchronous may happen inside this function."
Your First Async Function
Example:
async function greet() {
return "Hello Developer";
}
Looks normal.
But something interesting happens.
When we call:
greet();
JavaScript does NOT return:
"Hello Developer"
Instead it returns:
Promise { "Hello Developer" }
Why?
Because every async function automatically returns a Promise.
Visualizing Async Functions
Normal function:
function greet() {
return "Hello";
}
Returns:
"Hello"
Async function:
async function greet() {
return "Hello";
}
Returns:
Promise.resolve("Hello")
JavaScript automatically wraps the value inside a Promise.
What Does await Mean?
Now comes the interesting part.
Consider:
const data = await fetchData();
The keyword:
await
means:
Pause this async function
until the Promise completes.
Not the entire application.
Not JavaScript itself.
Only the current async function.
This distinction is extremely important.
Restaurant Analogy for await
Imagine a waiter submits an order to the kitchen.
Instead of repeatedly asking:
Is it ready?
Is it ready?
Is it ready?
The waiter simply waits for the kitchen notification.
Once the food is ready:
Work continues.
That is essentially how await behaves.
A Simple Example
Promise:
function getFood() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Burger");
}, 2000);
});
}
Using Async/Await:
async function orderFood() {
const food = await getFood();
console.log(food);
}
orderFood();
Output after 2 seconds:
Burger
The code reads from top to bottom.
Just like normal JavaScript.
Comparing Promise vs Async Await
Promise version:
getUser()
.then(user => {
return getOrders(user);
})
.then(orders => {
console.log(orders);
});
Async/Await version:
const user = await getUser();
const orders = await getOrders(user);
console.log(orders);
Which is easier to read?
Most developers choose the second version.
Real World AQAD Example
Imagine a retailer logs into the platform.
Steps:
Verify Login
↓
Fetch Profile
↓
Fetch Orders
↓
Fetch Analytics
Promise version:
login()
.then(getProfile)
.then(getOrders)
.then(getAnalytics);
Async/Await version:
const user = await login();
const profile = await getProfile(user);
const orders = await getOrders(profile);
const analytics = await getAnalytics(profile);
It feels almost like reading English.
Error Handling with try...catch
One reason developers love Async/Await is error handling.
Promise version:
fetchData()
.then(processData)
.catch(error => {
console.log(error);
});
Works.
But larger applications become messy.
Async/Await provides:
try {
const data = await fetchData();
console.log(data);
} catch(error) {
console.log(error);
}
This resembles traditional programming languages.
Much easier to understand.
Understanding try
Think of:
try
as saying:
Attempt this operation.
Example:
try {
processPayment();
}
Understanding catch
If something fails:
catch
handles the error.
Think:
If something goes wrong,
handle it here.
Real Banking Example
Suppose a payment is processed.
Possible problems:
Card Expired
Network Failure
Insufficient Funds
Server Error
Example:
try {
const payment = await processPayment();
console.log("Success");
}
catch(error) {
console.log("Payment Failed");
}
This pattern appears everywhere in production applications.
Sequential Execution
Consider:
const user = await getUser();
const orders = await getOrders(user);
const invoice = await createInvoice(orders);
Execution happens one step at a time.
Get User
↓
Get Orders
↓
Create Invoice
This is called sequential execution.
Sometimes this is exactly what we want.
When Sequential Execution Becomes Slow
Imagine:
await getProducts();
await getCategories();
await getBrands();
Each request takes:
2 seconds
Total time:
6 seconds
That's inefficient.
The requests don't depend on each other.
They can run simultaneously.
Promise.all with Async Await
Better approach:
const results = await Promise.all([
getProducts(),
getCategories(),
getBrands()
]);
Now all requests run together.
Total time:
Approximately 2 seconds
This is a common optimization technique.
Real E-Commerce Example
Homepage loads:
Products
Categories
Brands
Offers
Instead of loading one by one:
await Promise.all([
fetchProducts(),
fetchCategories(),
fetchBrands(),
fetchOffers()
]);
The page becomes significantly faster.
Common Beginner Mistake #1
Using await outside async functions.
Wrong:
const data = await fetchData();
Output:
SyntaxError
Correct:
async function load() {
const data = await fetchData();
}
Common Beginner Mistake #2
Forgetting Error Handling
Wrong:
const user = await getUser();
Better:
try {
const user = await getUser();
}
catch(error) {
console.log(error);
}
Production applications always expect failures.
Common Beginner Mistake #3
Awaiting Everything
Many beginners do:
await taskOne();
await taskTwo();
await taskThree();
Even when tasks are unrelated.
Sometimes:
Promise.all()
is faster.
Good developers understand the difference.
Behind The Scenes
This is important.
Async/Await is NOT a replacement for Promises.
It is built on top of Promises.
Think of it this way:
Promises = Engine
Async/Await = Better Steering Wheel
Underneath:
JavaScript is still using Promises.
Async/Await simply makes the code easier to write.
How Modern Backend Developers Use Async Await
If you look at modern Node.js projects:
You'll see Async/Await everywhere.
Example:
const user = await User.findById(id);
const orders = await Order.find({
userId: id
});
const analytics = await getAnalytics(id);
This style dominates modern backend development.
Because it is:
Clean
Readable
Maintainable
AQAD Marketplace Example
Imagine a vendor dashboard.
When the page loads:
Vendor Profile
Orders
Products
Revenue Analytics
Using Async/Await:
const profile = await getVendorProfile();
const orders = await getOrders();
const products = await getProducts();
const analytics = await getAnalytics();
Any developer joining the project can quickly understand the flow.
That is one of the biggest benefits of Async/Await.
Async Await Interview Question
What is the output?
async function test() {
return 10;
}
console.log(test());
Output:
Promise { 10 }
Because every async function returns a Promise.
This is a very common interview question.
Why Async Await Became So Popular
Because it solved a real problem.
Developers wanted:
Readable Async Code
And Async/Await delivered exactly that.
Today, most production JavaScript applications use Async/Await extensively.
Whether you're building:
React Applications
Node.js APIs
Express Servers
AWS Lambda Functions
Mobile Applications
You'll encounter Async/Await everywhere.
Summary we learned:
Why Async/Await was introduced
async keyword
await keyword
Async functions return Promises
Error handling with try/catch
Sequential execution
Promise.all optimization
Common mistakes
Real-world backend examples
Think of Async/Await as a translator.
Promises are still doing the work underneath.
But Async/Await translates complex asynchronous logic into code that feels natural and easy to read.
And now that we understand modern asynchronous programming, it's time to explore another topic that appears frequently in interviews and production code.
0 Comments