Introduction
Promises revolutionized JavaScript async programming. A Promise represents a value that may not exist yet—the eventual result of an async operation. Unlike callbacks, promises can be chained, composed, and handled with cleaner error management.
Key Concepts
Promise: An object representing the eventual completion or failure of an async operation.
States: pending (initial), fulfilled (success), rejected (failure). Once settled, cannot change.
Thenable: Any object with a .then() method.
Real World Context
Every fetch() returns a Promise. APIs return promises. Modern libraries use promises. Understanding them is required for any async work in JavaScript.
Deep Dive
Promise States
javascript// Pending -> Fulfilled const fulfilled = new Promise((resolve, reject) => { setTimeout(() => resolve('Success!'), 1000); }); // Pending -> Rejected const rejected = new Promise((resolve, reject) => { setTimeout(() => reject(new Error('Failed!')), 1000); }); // State is permanent once settled const promise = new Promise((resolve) => { resolve('first'); // This wins resolve('second'); // Ignored reject('error'); // Ignored });
Creating Promises
javascript// Constructor const promise = new Promise((resolve, reject) => { const success = doAsyncWork(); if (success) { resolve(result); } else { reject(new Error('Failed')); } }); // Static methods for immediate values Promise.resolve('immediate value'); Promise.reject(new Error('immediate error'));
Consuming with .then() and .catch()
javascriptfetch('/api/user') .then(response => response.json()) // Returns new promise .then(user => { console.log('User:', user); return user.id; // Passed to next .then() }) .then(userId => fetch(`/api/posts/${userId}`)) .catch(error => { // Catches ANY error in the chain console.error('Failed:', error); }) .finally(() => { // Always runs, no value passed hideLoadingSpinner(); });
Promise Chaining
javascript// Each .then() returns a new Promise Promise.resolve(1) .then(x => x + 1) // 2 .then(x => x * 2) // 4 .then(x => x + 10) // 14 .then(console.log); // Logs 14 // Returning a Promise waits for it Promise.resolve('start') .then(() => fetch('/api/data')) // Returns Promise .then(response => response.json()) // Waits for fetch .then(data => console.log(data));
Error Propagation
javascriptPromise.resolve('start') .then(() => { throw new Error('Oops!'); }) .then(() => console.log('Skipped')) // Skipped! .then(() => console.log('Also skipped')) // Skipped! .catch(e => console.log('Caught:', e.message)) // 'Caught: Oops!' .then(() => console.log('Continues')); // Runs!
Common Pitfalls
- Forgetting to return in .then(): Next handler gets undefined.
- Nested promises: Use return, not nested .then().
- Missing .catch(): Unhandled rejection warnings.
Best Practices
- Always return in .then(): Chain properly.
- End chains with .catch(): Handle all errors.
- Use .finally() for cleanup: Loading spinners, etc.
- Prefer async/await for readability: When possible.
Summary
Promises represent async operation results with three states: pending, fulfilled, rejected. Chain with .then(), handle errors with .catch(), clean up with .finally(). Always return values in .then() handlers. Errors propagate down the chain until caught.
Code Examples
// Pending -> Fulfilled
const fulfilled = new Promise((resolve, reject) => {
setTimeout(() => resolve('Success!'), 1000);
});
// Pending -> Rejected
const rejected = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Failed!')), 1000);
});
// State is permanent once settled
const promise = new Promise((resolve) => {
resolve('first'); // This wins
resolve('second'); // Ignored
reject('error'); // Ignored
});// Constructor
const promise = new Promise((resolve, reject) => {
const success = doAsyncWork();
if (success) {
resolve(result);
} else {
reject(new Error('Failed'));
}
});
// Static methods for immediate values
Promise.resolve('immediate value');
Promise.reject(new Error('immediate error'));