In the previous tutorial, we learned about callbacks and the dreaded "Callback Hell". To solve the problem of deeply nested callbacks, JavaScript introduced Promises.
Promises are a much cleaner, more elegant way to handle asynchronous operations in Node.js. They provide better readability and superior error handling.
Imagine you order a coffee at a busy cafe. The cashier takes your money and hands you a receipt with a ticket number. That receipt is a Promise. It is a guarantee that eventually, you will receive either your coffee or a reason why they cannot fulfill your order (e.g., they ran out of milk).
In programming, a Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
A Promise can only be in one of three states at any given time:
You can create a new Promise using the new Promise constructor. It takes a function with two arguments: resolve and reject.
resolve(data) when your task is successful.reject(error) when your task fails.
const myPromise = new Promise((resolve, reject) => {
let success = true; // Change this to false to see the rejection!
setTimeout(() => {
if (success) {
resolve("Operation was a success! Here is your data.");
} else {
reject("Operation failed! Something went wrong.");
}
}, 2000); // Simulating a 2-second delay
});
myPromise
.then((message) => {
console.log(message);
})
.catch((error) => {
console.error(error);
});
.then and .catch)Creating a promise is only half the battle. Once you have a promise, you need to "consume" or handle its result. We do this using .then() for successes and .catch() for errors.
const myPromise = Promise.resolve("Operation was a success! Here is your data.");
myPromise
.then((message) => {
// This runs if resolve() was called
console.log("Success:", message);
})
.catch((error) => {
// This runs if reject() was called
console.error("Error:", error);
})
.finally(() => {
// This runs regardless of success or failure
console.log("Promise processing is finished.");
});
The .finally() block is optional but very useful for running cleanup code (like closing a loading spinner or a database connection), regardless of whether the promise was resolved or rejected.
The biggest advantage of Promises over raw callbacks is chaining. Instead of nesting code deeper and deeper, you can chain multiple .then() methods.
When you return a value inside a .then(), it automatically gets wrapped in a new Promise, allowing the next .then() to receive it.
const fetchNumber = new Promise((resolve, reject) => resolve(10));fetchNumber .then((num) => { console.log("Started with:", num); return num * 2; // Returns 20 to the next .then() }) .then((num) => { console.log("Multiplied by 2:", num); return num * 5; // Returns 100 to the next .then() }) .then((finalNum) => { console.log("Final result:", finalNum); }) .catch((err) => { // If ANY of the promises above fail, it instantly jumps here! console.error("Caught an error in the chain:", err); });
Notice how much cleaner this is! The code reads from top to bottom, rather than diagonally inwards like Callback Hell. Furthermore, a single .catch() at the end will catch errors from any step in the chain.
Modern Node.js comes with Promise-based versions of its core modules, so you rarely have to create your own wrappers. For example, instead of using the callback-based fs.readFile, you can use fs.promises.
const fs = require('fs').promises;
fs.readFile('example.txt', 'utf8')
.then((data) => {
console.log("File content:", data);
})
.catch((err) => {
console.error("Could not read file:", err.message);
});
.catch(): Unhandled promise rejections can crash your Node.js application. Always handle errors at the end of your promise chains.return the promise chain so the caller can also chain .then() and .catch().Which of the following is NOT a valid state for a Promise?