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()andreject() - 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
Eventually, one of two things happens:
Order placed
↓
Waiting
/ \
/ \
Delivered Cancelled
A Promise works similarly.
Promise
|
Pending
/ \
/ \
Fulfilled Rejected
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
});
The Promise constructor receives a function with two parameters:
(resolve, reject) => {
}
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");
});
When resolve() is called:
Pending
↓
Fulfilled
The value passed to resolve() becomes the result of the Promise.
Here:
resolve("Operation successful");
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");
});
The Promise changes from:
Pending
↓
Rejected
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
});
State:
Pending
Fulfilled
The operation completed successfully.
const promise = new Promise((resolve, reject) => {
resolve("Success");
});
State transition:
Pending → Fulfilled
Rejected
The operation failed.
const promise = new Promise((resolve, reject) => {
reject("Failed");
});
State transition:
Pending → Rejected
A Promise can settle only once.
For example:
const promise = new Promise((resolve, reject) => {
resolve("Success");
reject("Failed");
});
The first settlement wins.
The Promise becomes fulfilled with:
Success
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);
});
Output:
Login successful
The important connection is:
resolve("Login successful");
passes the value to:
.then((result) => {
console.log(result);
});
So:
resolve("Login successful")
↓
result = "Login successful"
↓
.then()
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);
});
What happens?
Step 1
The Promise starts in the pending state.
Pending
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");
runs.
The state changes:
Pending → Fulfilled
Step 5
The .then() callback executes.
Output:
Data received
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);
});
Output:
Login failed
The flow is:
reject()
↓
Rejected
↓
.catch()
A common pattern is:
promise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.log(error);
});
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");
});
Output:
Success
Operation finished
If the Promise is rejected:
Promise.reject("Failed")
.catch((error) => {
console.log(error);
})
.finally(() => {
console.log("Operation finished");
});
Output:
Failed
Operation finished
A common use case is a loading indicator:
showLoading();
fetchData()
.then((data) => {
console.log(data);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
hideLoading();
});
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
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);
});
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);
});
Output:
10
20
25
Why?
The first Promise contains:
10
The first .then() receives:
value = 10
It returns:
return value * 2;
which produces:
20
The next .then() receives 20.
It returns:
25
The final .then() receives 25.
Therefore:
10
↓
20
↓
25
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
Using callbacks:
loginUser(function(user) {
getUser(user.id, function(userData) {
getPosts(userData.id, function(posts) {
getComments(posts[0].id, function(comments) {
console.log(comments);
});
});
});
});
Notice how the functions become deeply nested.
This is commonly called Callback Hell.
The structure starts looking like:
loginUser
└── getUser
└── getPosts
└── getComments
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);
});
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);
});
Output:
10
Something went wrong
What happened?
The first .then() executed:
console.log(value);
So we get:
10
Then an error was thrown:
throw new Error("Something went wrong");
The Promise chain now becomes rejected.
The next .then() is skipped:
.then(() => {
console.log("This will not execute");
})
The error moves to .catch():
.catch((error) => {
console.log(error.message);
});
So the flow is:
.then()
↓
Error occurs
↓
Promise becomes rejected
↓
Skip remaining .then()
↓
.catch()
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);
});
Output:
Start
Step 1
Error: Step 2 failed
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:
- Async orchestration
- 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
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;
}
Here:
const user = await getUser();
and:
const orders = await getOrders(user.id);
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;
}
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;
});
}
We can separate the responsibilities.
Data retrieval
function getUser() {
return fetch("/user")
.then(response => response.json());
}
Data retrieval
function getOrders(userId) {
return fetch("/orders/" + userId)
.then(response => response.json());
}
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;
}
Async orchestration
async function getUserTotal() {
const user = await getUser();
const orders = await getOrders(user.id);
return calculateTotal(orders);
}
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));
No API request is required.
Easier Maintenance
Suppose the discount changes from 10% to 20%.
You only modify:
if (total > 10000) {
total *= 0.8;
}
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);
It controls the sequence.
Business Logic asks:
What should we do with the data?
Example:
function calculateTotal(orders) {
// application rules
}
It contains the rules.
21. Complete Flow
Putting everything together:
USER REQUEST
|
↓
Async Orchestration
|
↓
getUser()
|
↓
getOrders()
|
↓
Business Logic
|
↓
calculateTotal()
|
↓
Result
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()
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);
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
And as applications grow, Separation of Concerns becomes equally important:
Async orchestration → controls the sequence
Business logic → contains the application rules
Top comments (0)