JavaScript Promises Explained for Beginners
Introduction
In the previous blog, we learned about callbacks and how they help JavaScript handle asynchronous tasks. But we also saw a major problem: callback nesting makes code hard to read and maintain. JavaScript developers wanted a better way to handle asynchronous operations, and that's when Promises were introduced. Think of a Promise like a real-world promise you make to a friend. You promise to call them back after finishing some work. You might succeed, or you might fail, but eventually, you'll deliver an answer. In this blog, we'll understand how JavaScript Promises work and why they're better than callbacks.
1. What Problem Do Promises Solve?
Let's start by comparing callbacks with promises.
The Callback Problem:
Remember our nested callback example from the last blog? It looked like a pyramid:
getUser(1, function(user) {
getOrders(user, function(orders) {
processOrders(orders, function(result) {
sendEmail(result, function(emailResponse) {
updateDatabase(emailResponse, function(dbResult) {
console.log("Done: " + dbResult);
});
});
});
});
});
This is hard to read and maintain. Imagine adding error handling or logging to each step. The code becomes a mess.
The Promise Solution:
With promises, you can write the same logic in a much cleaner way:
getUser(1)
.then(function(user) {
return getOrders(user);
})
.then(function(orders) {
return processOrders(orders);
})
.then(function(result) {
return sendEmail(result);
})
.then(function(emailResponse) {
return updateDatabase(emailResponse);
})
.then(function(dbResult) {
console.log("Done: " + dbResult);
});
See the difference? The code reads from top to bottom, like a story. It's much easier to understand.
The Key Difference:
With callbacks, you pass a function that will be called later
With promises, you get an object that represents the future value, and you can attach handlers to it
Promises are essentially a better way to handle asynchronous code.
2. Understanding Promises as Future Values
A Promise is an object that represents a value that might not be available right now, but will be available in the future.
Imagine you order food at a restaurant in Jaipur. When you place the order, the waiter doesn't hand you the food immediately. Instead, he gives you a token (a promise) that says "Your food will be ready in 30 minutes." You can then do other things while waiting. When your food is ready, the restaurant calls your name, and you go pick it up.
The Promise works similarly:
You create a promise (place an order)
The promise is pending (waiting for food)
Eventually, the promise is either fulfilled (food is ready) or rejected (restaurant is out of that ingredient)
Once resolved, you can handle the result
Creating a Simple Promise:
// A promise that represents getting data from a server
const dataPromise = new Promise(function(resolve, reject) {
// Simulating a server request that takes 2 seconds
setTimeout(function() {
const data = "User data from server";
resolve(data); // The promise is fulfilled
}, 2000);
});
console.log(dataPromise);
// Promise { <pending> }
When you create a promise, it's immediately in a pending state.
3. Promise States: Pending, Fulfilled, and Rejected
A Promise has three possible states, and it can only be in one of them at a time.
State 1: Pending
This is the initial state. The operation hasn't completed yet.
const promise = new Promise(function(resolve, reject) {
console.log("Promise is pending");
// We haven't called resolve or reject yet
});
console.log(promise); // Promise { <pending> }
State 2: Fulfilled (Resolved)
When the operation completes successfully, the promise is fulfilled. You call the resolve function to mark it as fulfilled.
const successPromise = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve("Operation successful!");
}, 1000);
});
// After 1 second, the promise state changes to fulfilled
State 3: Rejected
If something goes wrong, the promise is rejected. You call the reject function to mark it as rejected.
const failurePromise = new Promise(function(resolve, reject) {
setTimeout(function() {
reject("Something went wrong!");
}, 1000);
});
// After 1 second, the promise state changes to rejected
Important: Once a promise is fulfilled or rejected, it cannot change states. It's permanent. We say the promise is "settled."
const fixedPromise = new Promise(function(resolve, reject) {
resolve("Success!");
reject("This will be ignored"); // This has no effect
});
4. The Promise Lifecycle
Let's see the complete journey of a promise from creation to resolution.
Step 1: Create a Promise
const fetchUserData = new Promise(function(resolve, reject) {
console.log("1. Promise created, fetching data...");
setTimeout(function() {
console.log("3. Data received from server");
const user = { id: 1, name: "Priya Singh", city: "Delhi" };
resolve(user); // Promise is now fulfilled
}, 2000);
console.log("2. Request sent to server");
});
Step 2: Handle the Result with .then() and .catch()
fetchUserData
.then(function(user) {
console.log("4. Handling success:", user);
})
.catch(function(error) {
console.log("4. Handling error:", error);
});
console.log("2b. Code continues without waiting");
// Output:
// 1. Promise created, fetching data...
// 2. Request sent to server
// 2b. Code continues without waiting
// (2 seconds pass)
// 3. Data received from server
// 4. Handling success: { id: 1, name: "Priya Singh", city: "Delhi" }
Timeline visualization:
Time 0ms: Create promise (pending)
Execute synchronous code
Attach .then() handler
Time 2000ms: resolve(user) is called
Promise becomes fulfilled
.then() handler is triggered
Time 2001ms: Callback function inside .then() executes
Final output is displayed
5. Handling Success and Failure
Now let's learn how to properly handle both successful and failed promises.
Using .then() for Success:
function fetchUserProfile(userId) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
if (userId > 0) {
const profile = { id: userId, name: "Amit Verma", status: "Active" };
resolve(profile);
} else {
reject("Invalid user ID");
}
}, 1500);
});
}
fetchUserProfile(5)
.then(function(profile) {
console.log("Profile loaded:", profile.name);
});
Using .catch() for Errors:
fetchUserProfile(-1)
.catch(function(error) {
console.log("Error occurred:", error);
});
Handling Both Cases with .then() and .catch():
fetchUserProfile(5)
.then(function(profile) {
console.log("Success:", profile.name);
})
.catch(function(error) {
console.log("Error:", error);
});
Using .finally() for Cleanup:
Sometimes you want to execute code whether the promise succeeds or fails. That's where .finally() comes in:
fetchUserProfile(5)
.then(function(profile) {
console.log("Success:", profile.name);
})
.catch(function(error) {
console.log("Error:", error);
})
.finally(function() {
console.log("Request completed, cleanup done");
});
// Output:
// Success: Amit Verma
// Request completed, cleanup done
The .finally() block executes no matter what, making it perfect for closing connections, hiding loading spinners, or other cleanup tasks.
6. Promise Chaining: The Real Power
Promise chaining is what makes promises so much better than callbacks. You can chain multiple promises together, and the result of one becomes the input of the next.
Basic Promise Chaining:
function getUser(userId) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve({ id: userId, name: "Rahul Sharma" });
}, 1000);
});
}
function getOrders(user) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(["Laptop", "Phone", "Tablet"]);
}, 1000);
});
}
function processOrders(orders) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve("Orders processed. Total: " + orders.length);
}, 1000);
});
}
// Chaining promises
getUser(1)
.then(function(user) {
console.log("User retrieved:", user.name);
return getOrders(user);
})
.then(function(orders) {
console.log("Orders retrieved:", orders);
return processOrders(orders);
})
.then(function(result) {
console.log("Final result:", result);
})
.catch(function(error) {
console.log("Something went wrong:", error);
});
// Output:
// User retrieved: Rahul Sharma
// Orders retrieved: ["Laptop", "Phone", "Tablet"]
// Final result: Orders processed. Total: 3
Notice how each .then() receives the result from the previous promise. This is much cleaner than nested callbacks.
Key Rule: Return a Promise from .then()
For chaining to work, each .then() must return a value or a promise:
getUser(1)
.then(function(user) {
return user; // Return the user object
})
.then(function(user) {
console.log("Received:", user);
return getOrders(user); // Return a new promise
})
.then(function(orders) {
console.log("Orders:", orders);
return "All done"; // Return a simple value
})
.then(function(message) {
console.log(message); // All done
});
7. Promise vs Callback Comparison
Let's side-by-side compare how you'd solve the same problem with callbacks versus promises.
Using Callbacks (Pyramid of Doom):
function processUserWithCallbacks(userId) {
getUser(userId, function(user) {
console.log("User:", user.name);
getOrders(user, function(orders) {
console.log("Orders:", orders.length);
processOrders(orders, function(result) {
console.log("Result:", result);
// Error handling at this deep level is messy
});
});
});
}
Using Promises (Clean and Readable):
function processUserWithPromises(userId) {
return getUser(userId)
.then(function(user) {
console.log("User:", user.name);
return getOrders(user);
})
.then(function(orders) {
console.log("Orders:", orders.length);
return processOrders(orders);
})
.then(function(result) {
console.log("Result:", result);
})
.catch(function(error) {
console.log("Error anywhere in chain:", error);
});
}
Advantages of Promises:
Linear flow: Code reads top to bottom, like a story
Single error handler: One
.catch()handles errors from any stepEasier to modify: Adding a step doesn't require more nesting
Better readability: Even someone new to the code can understand it
Composability: You can combine multiple promises easily
8. Real-World Example: Complete Promise Chain
Let's build a realistic example of fetching and processing data.
// Simulating API calls
function fetchUserFromAPI(userId) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
if (userId > 0) {
resolve({
id: userId,
name: "Neha Kapoor",
email: "neha@example.com"
});
} else {
reject("Invalid user ID");
}
}, 1000);
});
}
function fetchUserOrders(user) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve({
userId: user.id,
orders: [
{ id: 1, product: "Book", price: 250 },
{ id: 2, product: "Pen Set", price: 150 }
]
});
}, 1000);
});
}
function calculateTotal(orderData) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
const total = orderData.orders.reduce(function(sum, order) {
return sum + order.price;
}, 0);
resolve({
userId: orderData.userId,
itemCount: orderData.orders.length,
total: total
});
}, 500);
});
}
// Using the promise chain
fetchUserFromAPI(1)
.then(function(user) {
console.log("User loaded:", user.name);
return fetchUserOrders(user);
})
.then(function(orderData) {
console.log("Orders loaded:", orderData.orders.length, "items");
return calculateTotal(orderData);
})
.then(function(summary) {
console.log("Summary:");
console.log("- Total items:", summary.itemCount);
console.log("- Total amount: Rs." + summary.total);
})
.catch(function(error) {
console.log("Error:", error);
})
.finally(function() {
console.log("Request completed");
});
// Output:
// User loaded: Neha Kapoor
// Orders loaded: 2 items
// Summary:
// - Total items: 2
// - Total amount: Rs.400
// Request completed
9. Promise Lifecycle Diagram
Here's a visual representation of how a promise flows through its lifecycle:
Start
|
v
new Promise() created
|
v (PENDING STATE)
Executor function runs
|
+--------+--------+
| |
v v
resolve() called reject() called
| |
v v
FULFILLED REJECTED
| |
+--------+--------+
|
v
.then() or .catch()
handler executes
|
v
End
The promise can only take one path: either to fulfilled or rejected, never both.
Conclusion
Promises are a major improvement over callbacks for handling asynchronous operations in JavaScript. They provide:
A cleaner, more readable way to write asynchronous code
Better error handling with a single
.catch()handlerThe ability to chain operations in a logical sequence
A standard way to handle async operations across the JavaScript ecosystem
While promises are much better than callbacks, there's an even newer syntax called async/await that makes promise code even easier to read. But understanding promises deeply is crucial because async/await is built on top of promises.
Start using promises in your projects, and you'll notice how much cleaner your asynchronous code becomes. The pyramid of doom will be a thing of the past!




