Ticker

6/recent/ticker-posts

Chapter 13: Promises & Async Await Explained Through Real-World Examples



 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:

  1. Login the user

  2. Fetch user details

  3. Fetch user orders

  4. 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.


Post a Comment

0 Comments