DEV Community

Sai Swaroop Bijinapalli
Sai Swaroop Bijinapalli

Posted on

About Promises in JS

Perfect. Here is a ready-to-publish beginner-friendly blog that starts from Promises and gradually covers all the concepts you studied.

Asynchronous JavaScript: Understanding Promises, Chaining, Error Handling, and Separation of Concerns

JavaScript is often described as a single-threaded programming language. This means JavaScript executes one piece of code at a time. But modern applications constantly perform operations that take time, such as API requests, database operations, file handling, and timers.

If JavaScript waited for every operation to finish before continuing, applications would become slow and unresponsive.

This is where asynchronous JavaScript comes into the picture.

One of the most important tools for handling asynchronous operations is the Promise.

In this blog, we will understand Promises from the basics and gradually explore:

  • What a Promise is
  • resolve() and reject()
  • Promise states
  • .then()
  • .catch()
  • .finally()
  • Promise chaining
  • Error propagation
  • Callback Hell
  • Separation of concerns
  • Async orchestration vs business logic

1. What Is a Promise?

A Promise is an object that represents the eventual result of an asynchronous operation.

The result may be:

  • Successful
  • Failed
  • Still in progress

For example, imagine ordering food online.

Initially:

Order placed
     ↓
   Waiting
Enter fullscreen mode Exit fullscreen mode

Eventually, one of two things happens:

Order placed
     ↓
   Waiting
    /    \
   /      \
Delivered  Cancelled
Enter fullscreen mode Exit fullscreen mode

A Promise works similarly.

              Promise
                 |
              Pending
             /       \
            /         \
     Fulfilled       Rejected
Enter fullscreen mode Exit fullscreen mode

A Promise doesn't immediately give you the final result. Instead, it gives you a way to handle the result when it becomes available.


2. Creating a Promise

A Promise is created using the Promise constructor.

const promise = new Promise((resolve, reject) => {

    // asynchronous operation

});
Enter fullscreen mode Exit fullscreen mode

The Promise constructor receives a function with two parameters:

(resolve, reject) => {

}
Enter fullscreen mode Exit fullscreen mode

These two functions are provided by JavaScript.

  • resolve() → tells the Promise that the operation succeeded.
  • reject() → tells the Promise that the operation failed.

3. Why Do We Use resolve()?

resolve() is used when an asynchronous operation completes successfully.

const promise = new Promise((resolve, reject) => {

    resolve("Operation successful");

});
Enter fullscreen mode Exit fullscreen mode

When resolve() is called:

Pending
   ↓
Fulfilled
Enter fullscreen mode Exit fullscreen mode

The value passed to resolve() becomes the result of the Promise.

Here:

resolve("Operation successful");
Enter fullscreen mode Exit fullscreen mode

means:

"The operation was successful, and this is the result."


4. Why Do We Use reject()?

reject() is used when the asynchronous operation fails.

const promise = new Promise((resolve, reject) => {

    reject("Operation failed");

});
Enter fullscreen mode Exit fullscreen mode

The Promise changes from:

Pending
   ↓
Rejected
Enter fullscreen mode Exit fullscreen mode

The value passed to reject() represents the reason for the failure.


5. Promise States

A Promise has three states.

Pending

The operation is still in progress.

const promise = new Promise((resolve, reject) => {

    // still running

});
Enter fullscreen mode Exit fullscreen mode

State:

Pending
Enter fullscreen mode Exit fullscreen mode

Fulfilled

The operation completed successfully.

const promise = new Promise((resolve, reject) => {

    resolve("Success");

});
Enter fullscreen mode Exit fullscreen mode

State transition:

Pending → Fulfilled
Enter fullscreen mode Exit fullscreen mode

Rejected

The operation failed.

const promise = new Promise((resolve, reject) => {

    reject("Failed");

});
Enter fullscreen mode Exit fullscreen mode

State transition:

Pending → Rejected
Enter fullscreen mode Exit fullscreen mode

A Promise can settle only once.

For example:

const promise = new Promise((resolve, reject) => {

    resolve("Success");
    reject("Failed");

});
Enter fullscreen mode Exit fullscreen mode

The first settlement wins.

The Promise becomes fulfilled with:

Success
Enter fullscreen mode Exit fullscreen mode

The later reject() has no effect.


6. Using .then()

Once a Promise is fulfilled, we can use .then() to handle the successful result.

const promise = new Promise((resolve, reject) => {

    resolve("Login successful");

});

promise.then((result) => {

    console.log(result);

});
Enter fullscreen mode Exit fullscreen mode

Output:

Login successful
Enter fullscreen mode Exit fullscreen mode

The important connection is:

resolve("Login successful");
Enter fullscreen mode Exit fullscreen mode

passes the value to:

.then((result) => {
    console.log(result);
});
Enter fullscreen mode Exit fullscreen mode

So:

resolve("Login successful")
              ↓
       result = "Login successful"
              ↓
            .then()
Enter fullscreen mode Exit fullscreen mode

7. Promise with setTimeout()

Let's make the operation asynchronous.

const promise = new Promise((resolve, reject) => {

    setTimeout(() => {

        resolve("Data received");

    }, 2000);

});

promise.then((data) => {

    console.log(data);

});
Enter fullscreen mode Exit fullscreen mode

What happens?

Step 1

The Promise starts in the pending state.

Pending
Enter fullscreen mode Exit fullscreen mode

Step 2

setTimeout() starts a two-second timer.

The Promise is still pending.

Step 3

.then() waits for the Promise to become fulfilled.

Step 4

After two seconds:

resolve("Data received");
Enter fullscreen mode Exit fullscreen mode

runs.

The state changes:

Pending → Fulfilled
Enter fullscreen mode Exit fullscreen mode

Step 5

The .then() callback executes.

Output:

Data received
Enter fullscreen mode Exit fullscreen mode

This is why we often see resolve() inside a setTimeout() when learning Promises.

setTimeout() provides the delay, while resolve() tells the Promise:

"The asynchronous operation has now completed successfully."


8. Using .catch()

.catch() is used to handle a rejected Promise.

const promise = new Promise((resolve, reject) => {

    reject("Login failed");

});

promise.catch((error) => {

    console.log(error);

});
Enter fullscreen mode Exit fullscreen mode

Output:

Login failed
Enter fullscreen mode Exit fullscreen mode

The flow is:

reject()
   ↓
Rejected
   ↓
.catch()
Enter fullscreen mode Exit fullscreen mode

A common pattern is:

promise
    .then((result) => {
        console.log(result);
    })
    .catch((error) => {
        console.log(error);
    });
Enter fullscreen mode Exit fullscreen mode

Here:

  • .then() handles success.
  • .catch() handles failure.

9. Using .finally()

Sometimes we need some code to run regardless of whether the Promise succeeds or fails.

That's where .finally() is useful.

Promise.resolve("Success")

    .then((result) => {

        console.log(result);

    })

    .finally(() => {

        console.log("Operation finished");

    });
Enter fullscreen mode Exit fullscreen mode

Output:

Success
Operation finished
Enter fullscreen mode Exit fullscreen mode

If the Promise is rejected:

Promise.reject("Failed")

    .catch((error) => {

        console.log(error);

    })

    .finally(() => {

        console.log("Operation finished");

    });
Enter fullscreen mode Exit fullscreen mode

Output:

Failed
Operation finished
Enter fullscreen mode Exit fullscreen mode

A common use case is a loading indicator:

showLoading();

fetchData()

    .then((data) => {
        console.log(data);
    })

    .catch((error) => {
        console.log(error);
    })

    .finally(() => {
        hideLoading();
    });
Enter fullscreen mode Exit fullscreen mode

Whether the request succeeds or fails, the loading indicator should disappear.


10. Promise Chaining

One of the biggest advantages of Promises is chaining.

Suppose we need to perform these operations:

Get User
   ↓
Get Profile
   ↓
Get Posts
   ↓
Display Posts
Enter fullscreen mode Exit fullscreen mode

With Promises:

getUser()

    .then((user) => {

        return getProfile(user.id);

    })

    .then((profile) => {

        return getPosts(profile.id);

    })

    .then((posts) => {

        console.log(posts);

    })

    .catch((error) => {

        console.log(error);

    });
Enter fullscreen mode Exit fullscreen mode

This is called Promise chaining.


11. How Does Promise Chaining Work?

Consider this example:

Promise.resolve(10)

    .then((value) => {

        console.log(value);

        return value * 2;

    })

    .then((value) => {

        console.log(value);

        return value + 5;

    })

    .then((value) => {

        console.log(value);

    });
Enter fullscreen mode Exit fullscreen mode

Output:

10
20
25
Enter fullscreen mode Exit fullscreen mode

Why?

The first Promise contains:

10
Enter fullscreen mode Exit fullscreen mode

The first .then() receives:

value = 10
Enter fullscreen mode Exit fullscreen mode

It returns:

return value * 2;
Enter fullscreen mode Exit fullscreen mode

which produces:

20
Enter fullscreen mode Exit fullscreen mode

The next .then() receives 20.

It returns:

25
Enter fullscreen mode Exit fullscreen mode

The final .then() receives 25.

Therefore:

10
 ↓
20
 ↓
25
Enter fullscreen mode Exit fullscreen mode

The important rule is:

The value returned from one .then() becomes the value received by the next .then().


12. Callback Hell

Before Promises became widely used, asynchronous operations were commonly handled using callbacks.

Suppose we need:

Login
 ↓
Get User
 ↓
Get Posts
 ↓
Get Comments
Enter fullscreen mode Exit fullscreen mode

Using callbacks:

loginUser(function(user) {

    getUser(user.id, function(userData) {

        getPosts(userData.id, function(posts) {

            getComments(posts[0].id, function(comments) {

                console.log(comments);

            });

        });

    });

});
Enter fullscreen mode Exit fullscreen mode

Notice how the functions become deeply nested.

This is commonly called Callback Hell.

The structure starts looking like:

loginUser
   └── getUser
        └── getPosts
             └── getComments
Enter fullscreen mode Exit fullscreen mode

As the application grows, this can become difficult to:

  • Read
  • Debug
  • Maintain
  • Handle errors in

Promises provide a flatter structure:

loginUser()

    .then(getUser)
    .then(getPosts)
    .then(getComments)

    .then((comments) => {
        console.log(comments);
    })

    .catch((error) => {
        console.log(error);
    });
Enter fullscreen mode Exit fullscreen mode

13. Error Propagation

Another important Promise feature is error propagation.

Consider:

Promise.resolve(10)

    .then((value) => {

        console.log(value);

        throw new Error("Something went wrong");

    })

    .then(() => {

        console.log("This will not execute");

    })

    .catch((error) => {

        console.log(error.message);

    });
Enter fullscreen mode Exit fullscreen mode

Output:

10
Something went wrong
Enter fullscreen mode Exit fullscreen mode

What happened?

The first .then() executed:

console.log(value);
Enter fullscreen mode Exit fullscreen mode

So we get:

10
Enter fullscreen mode Exit fullscreen mode

Then an error was thrown:

throw new Error("Something went wrong");
Enter fullscreen mode Exit fullscreen mode

The Promise chain now becomes rejected.

The next .then() is skipped:

.then(() => {
    console.log("This will not execute");
})
Enter fullscreen mode Exit fullscreen mode

The error moves to .catch():

.catch((error) => {
    console.log(error.message);
});
Enter fullscreen mode Exit fullscreen mode

So the flow is:

.then()
   ↓
Error occurs
   ↓
Promise becomes rejected
   ↓
Skip remaining .then()
   ↓
.catch()
Enter fullscreen mode Exit fullscreen mode

This movement of an error through the Promise chain is called error propagation.


14. Errors Can Happen Anywhere in the Chain

Promise.resolve("Start")

    .then((value) => {

        console.log(value);

        return "Step 1";

    })

    .then((value) => {

        console.log(value);

        throw new Error("Step 2 failed");

    })

    .then((value) => {

        console.log("Step 3");

    })

    .catch((error) => {

        console.log("Error:", error.message);

    });
Enter fullscreen mode Exit fullscreen mode

Output:

Start
Step 1
Error: Step 2 failed
Enter fullscreen mode Exit fullscreen mode

Step 3 isn't executed because the previous .then() produced an error.

The error automatically propagates to .catch().


15. Separation of Concerns

As applications become larger, another important concept becomes necessary:

Separation of Concerns.

The basic idea is:

Each function should have a clear responsibility.

When working with asynchronous JavaScript, it is useful to separate:

  1. Async orchestration
  2. Business logic

16. What Is Async Orchestration?

Async orchestration is about controlling when and in what order asynchronous operations happen.

For example:

Get User
   ↓
Get Orders
   ↓
Calculate Total
   ↓
Return Result
Enter fullscreen mode Exit fullscreen mode

The orchestration controls this sequence.

Example:

async function getUserTotal() {

    const user = await getUser();

    const orders = await getOrders(user.id);

    const total = calculateTotal(orders);

    return total;
}
Enter fullscreen mode Exit fullscreen mode

Here:

const user = await getUser();
Enter fullscreen mode Exit fullscreen mode

and:

const orders = await getOrders(user.id);
Enter fullscreen mode Exit fullscreen mode

are part of async orchestration.

They determine the order of asynchronous operations.


17. What Is Business Logic?

Business logic contains the actual rules of the application.

For example:

function calculateTotal(orders) {

    let total = 0;

    for (const order of orders) {

        if (order.status === "completed") {
            total += order.price;
        }

    }

    if (total > 10000) {
        total = total * 0.9;
    }

    return total;
}
Enter fullscreen mode Exit fullscreen mode

This function doesn't care about:

  • Promises
  • APIs
  • fetch()
  • setTimeout()
  • async/await

It only knows:

"Given a list of orders, calculate the total according to the application's rules."

That's business logic.


18. Separating Async Orchestration and Business Logic

Instead of mixing everything:

function getUserTotal() {

    return fetch("/user")
        .then(response => response.json())
        .then(user => {

            return fetch("/orders/" + user.id);

        })
        .then(response => response.json())
        .then(orders => {

            let total = 0;

            for (const order of orders) {

                if (order.status === "completed") {
                    total += order.price;
                }

            }

            return total;

        });
}
Enter fullscreen mode Exit fullscreen mode

We can separate the responsibilities.

Data retrieval

function getUser() {

    return fetch("/user")
        .then(response => response.json());

}
Enter fullscreen mode Exit fullscreen mode

Data retrieval

function getOrders(userId) {

    return fetch("/orders/" + userId)
        .then(response => response.json());

}
Enter fullscreen mode Exit fullscreen mode

Business logic

function calculateTotal(orders) {

    let total = 0;

    for (const order of orders) {

        if (order.status === "completed") {
            total += order.price;
        }

    }

    if (total > 10000) {
        total *= 0.9;
    }

    return total;
}
Enter fullscreen mode Exit fullscreen mode

Async orchestration

async function getUserTotal() {

    const user = await getUser();

    const orders = await getOrders(user.id);

    return calculateTotal(orders);

}
Enter fullscreen mode Exit fullscreen mode

Now each function has a clear responsibility.


19. Why Is This Separation Useful?

Easier Testing

We can test the business logic independently.

const orders = [
    { status: "completed", price: 5000 },
    { status: "completed", price: 7000 },
    { status: "cancelled", price: 3000 }
];

console.log(calculateTotal(orders));
Enter fullscreen mode Exit fullscreen mode

No API request is required.


Easier Maintenance

Suppose the discount changes from 10% to 20%.

You only modify:

if (total > 10000) {
    total *= 0.8;
}
Enter fullscreen mode Exit fullscreen mode

The API/orchestration code doesn't need to change.


Better Reusability

The calculateTotal() function can be used by:

  • Customer website
  • Admin dashboard
  • Reports
  • Mobile application

because it doesn't depend on a particular API or UI.


20. Async Orchestration vs Business Logic

A simple way to remember the difference is:

Async Orchestration asks:

When should this operation happen?

Example:

const user = await getUser();
const orders = await getOrders(user.id);
const total = calculateTotal(orders);
Enter fullscreen mode Exit fullscreen mode

It controls the sequence.

Business Logic asks:

What should we do with the data?

Example:

function calculateTotal(orders) {

    // application rules

}
Enter fullscreen mode Exit fullscreen mode

It contains the rules.


21. Complete Flow

Putting everything together:

                USER REQUEST
                     |
                     ↓
            Async Orchestration
                     |
                     ↓
                getUser()
                     |
                     ↓
              getOrders()
                     |
                     ↓
              Business Logic
                     |
                     ↓
            calculateTotal()
                     |
                     ↓
                  Result
Enter fullscreen mode Exit fullscreen mode

The responsibilities are separated.


22. Promises: The Complete Picture

All the concepts we discussed are connected:

                       Promise
                          |
                       Pending
                      /       \
                     /         \
              resolve()       reject()
                  ↓               ↓
             Fulfilled         Rejected
                  |               |
                  ↓               ↓
               .then()         .catch()
                  |
            return value
                  |
                  ↓
             next .then()
                  |
               error
                  |
                  ↓
               .catch()
                  |
                  ↓
             .finally()
Enter fullscreen mode Exit fullscreen mode

And Promise chaining helps avoid deeply nested Callback Hell.


23. Quick Summary

Concept Meaning
Promise Represents the future result of an asynchronous operation
Pending Operation is still in progress
Fulfilled Operation completed successfully
Rejected Operation failed
resolve() Marks the Promise as fulfilled
reject() Marks the Promise as rejected
.then() Handles successful results
.catch() Handles errors/rejections
.finally() Runs regardless of success or failure
Promise chaining Passes results from one .then() to the next
Error propagation Errors move through the chain until handled by .catch()
Callback Hell Deeply nested asynchronous callbacks
Async orchestration Controls the order of asynchronous operations
Business logic Contains the actual rules of the application

Conclusion

Promises provide a structured way to work with asynchronous operations in JavaScript.

Instead of deeply nested callbacks, we can use Promise chains:

getUser()
    .then(getOrders)
    .then(calculateTotal)
    .catch(handleError)
    .finally(cleanup);
Enter fullscreen mode Exit fullscreen mode

The most important concepts to remember are:

resolve()  → Success → Fulfilled → .then()

reject()   → Failure → Rejected → .catch()

return     → Next .then()

throw      → Error → .catch()

finally    → Runs regardless of outcome
Enter fullscreen mode Exit fullscreen mode

And as applications grow, Separation of Concerns becomes equally important:

Async orchestration → controls the sequence

Business logic → contains the application rules
Enter fullscreen mode Exit fullscreen mode

Top comments (0)