Real-Life Example of a Promise
Imagine you're ordering food online. The moment you place your order, you are “promised” that food will be delivered. However, you don’t know exactly when it will arrive—maybe it’ll be on time, delayed, or it might even get canceled.
This is similar to how JavaScript promises work. When you request something (like data from an API), JavaScript returns a promise, which will either be resolved (data delivered) or rejected (error or failure to get data).
What is a Promise?
A Promise in JavaScript represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It is an object that acts as a placeholder for a value that will be available in the future, either successfully or with an error.
A Promise has three possible states:
- Pending: This is the initial state of the Promise. It means the asynchronous operation is not yet complete, and the final value (or error) is not yet available.
- Fulfilled (Resolved): In this state, the asynchronous operation has completed successfully, and the Promise has been resolved with a value. This value can be accessed in the
.then()method. - Rejected: In this state, the asynchronous operation has failed, and the Promise has been rejected with an error. The error can be handled using the
.catch()method.
Once a Promise transitions from the Pending state to either Fulfilled or Rejected, it becomes settled, and its state cannot change again. This immutability ensures predictable behavior when working with Promises.
A Promise is created using the Promise constructor, which takes an executor function as an argument. The executor function is automatically executed when the Promise is created and receives two arguments:
- resolve: A function that is called when the operation succeeds. It changes the Promise's state to Fulfilled.
- reject: A function that is called when the operation fails. It changes the Promise's state to Rejected.
The executor function is responsible for defining the asynchronous task and determining when to call resolve() or reject().
// Creating a new Promise with an executor function
let promise = new Promise(function(resolve, reject) {
// Simulate an asynchronous task using setTimeout
setTimeout(() => {
let taskSuccessful = true; // Change this to false to simulate failure
if (taskSuccessful) {
resolve("Task completed successfully");
} else {
reject("Task failed");
}
}, 1000); // Asynchronous task completes after 1 second
});
// Handling the resolved or rejected state of the promise
promise
.then((result) => {
console.log("Fulfilled:", result); // Output: "Fulfilled: Task completed successfully"
})
.catch((error) => {
console.error("Rejected:", error); // Output: "Rejected: Task failed" (if taskSuccessful is false)
});Immutability
Once a promise has been resolved (fulfilled) or rejected, its state cannot change again, even if you attempt to call resolve() or reject() again in the executor function. Any additional calls to resolve() or reject() are ignored.
const immutablePromise = new Promise((resolve, reject) => {
// First resolve, this will set the promise to fulfilled state
resolve("First resolution");
// Attempting to resolve again (this will be ignored)
resolve("Second resolution");
// Attempting to reject after resolve (this will also be ignored)
reject("Rejected after resolve");
});
immutablePromise
.then((result) => {
console.log("Fulfilled with:", result); // Output: "Fulfilled with: First resolution"
})
.catch((error) => {
console.log("Rejected with:", error);
});
Promise Handlers
A Promise handler refers to the methods used to handle the result (or outcome) of a JavaScript Promise once the asynchronous operation it represents is complete. Promises have built-in handlers, primarily then(), catch(), and finally()
- then(): This method is used to define a callback function that runs when the Promise is successfully resolved. It receives the resolved value of the Promise as an argument.
- catch(): This method is used to define a callback function that runs when the Promise is rejected or an error occurs during the execution of the Promise. It receives the error as an argument.
- finally(): This method defines a callback function that runs after the Promise is settled, regardless of whether it was resolved or rejected. It is typically used for cleanup tasks such as stopping a loading spinner or closing resources. Note that finally() does not receive the resolved value or rejection reason; it is only for finalizing operations.
let fetchData = new Promise((resolve, reject) => {
let isSuccess = true; // Change to false to simulate rejection
setTimeout(() => {
if (isSuccess) {
resolve("Data fetched successfully!");
} else {
reject("Failed to fetch data!");
}
}, 2000);
});
fetchData
.then((result) => {
console.log("Success: ", result); // Handles resolved value
})
.catch((error) => {
console.error("Error: ", error); // Handles rejection or errors
})
.finally(() => {
console.log("Operation complete."); // Always runs
});
Resolve and Reject Functions
When you create a promise, you provide a function that has two arguments: resolve and reject. These functions are used to communicate the outcome of the async operation:
- resolve(value): If the operation is successful, `resolve()` is called with the resulting value.
- reject(error): If the operation fails, `reject()` is called with an error or reason for failure.
Example:
function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve("Data fetched successfully");
} else {
reject("Failed to fetch data");
}
}, 2000);
});
}
getData()
.then(data => console.log(data))
.catch(error => console.error(error));Promise Chaining
Promise chaining allows you to chain multiple `then()` methods together to handle a sequence of asynchronous tasks. Each `then()` passes its result to the next `then()` in the chain.
Example:
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000);
})
.then(result => {
console.log(result); // 1
return result * 2;
})
.then(result => {
console.log(result); // 2
return result * 2;
})
.then(result => {
console.log(result); // 4
});Pros and Cons of Promises
Pros:
- Improves code readability compared to callback-based asynchronous code.
- Makes it easier to handle complex async operations using chaining and `catch()` for error handling.
- Helps avoid the "callback hell" problem.
Cons:
- Promises can be overused and lead to complex promise chains.
- Error handling can sometimes be tricky, especially in deeply nested chains.