Promises
Master JavaScript Promises — create, chain, and handle errors with .then(), .catch(), and .finally() to write clean asynchronous code.
Promises
Promises were introduced in ES6 (2015) to solve the problems of callback hell. A Promise represents a value that may not be available yet but will be resolved (or rejected) at some point in the future. Instead of nesting callbacks, Promises let you chain .then() calls in a flat, readable sequence.
What Is a Promise?
A Promise is an object that can be in one of three states:
| State | Meaning | Transition |
|---|---|---|
| Pending | The operation has not completed yet | Initial state |
| Fulfilled | The operation completed successfully | Pending → Fulfilled (via resolve) |
| Rejected | The operation failed | Pending → Rejected (via reject) |
Once a Promise settles (fulfilled or rejected), it cannot change state again. A fulfilled Promise stays fulfilled; a rejected Promise stays rejected.
Creating a Promise
const myPromise = new Promise(function(resolve, reject) {
// Simulate an async operation
setTimeout(function() {
const success = true;
if (success) {
resolve('Operation completed!');
} else {
reject('Something went wrong');
}
}, 2000);
});
The Promise constructor takes a function with two parameters — resolve and reject. Call resolve(value) when the operation succeeds and reject(reason) when it fails.
Consuming a Promise
myPromise
.then(function(result) {
console.log('Success:', result);
})
.catch(function(error) {
console.log('Error:', error);
});
// Output (after 2 seconds):
// Success: Operation completed!
| Method | When It Runs |
|---|---|
.then(callback) | When the Promise is fulfilled — receives the resolved value |
.catch(callback) | When the Promise is rejected — receives the error reason |
.finally(callback) | Always runs after settled — no arguments, useful for cleanup |
Wrapping AJAX in a Promise
function fetchData(url) {
return new Promise(function(resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject('HTTP Error: ' + xhr.status);
}
};
xhr.onerror = function() {
reject('Network error');
};
xhr.send();
});
}
// Usage:
fetchData('https://jsonplaceholder.typicode.com/todos/1')
.then(data => console.log(data))
.catch(err => console.log(err));
Wrapping an async operation in a Promise converts it from the callback pattern to the Promise pattern. The function returns a Promise instead of accepting a callback parameter.
Chaining Promises
The real power of Promises is chaining. Each .then() returns a new Promise, so you can chain them in a flat sequence instead of nesting:
fetchData('/api/users/1')
.then(function(user) {
console.log('Got user:', user.name);
return fetchData('/api/posts?userId=' + user.id);
})
.then(function(posts) {
console.log('Got posts:', posts.length);
return fetchData('/api/comments?postId=' + posts[0].id);
})
.then(function(comments) {
console.log('Got comments:', comments.length);
})
.catch(function(error) {
console.log('Error at any step:', error);
});
Callbacks vs Promises — Side by Side
// CALLBACK HELL
fetchUser(1, function(err, user) {
if (err) return console.log(err);
fetchPosts(user.id, function(err, posts) {
if (err) return console.log(err);
fetchComments(posts[0].id, function(err, comments) {
if (err) return console.log(err);
console.log(comments);
});
});
});
// PROMISE CHAIN
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => console.log(comments))
.catch(err => console.log(err));
The Promise version is flat, readable, and has a single .catch() that handles errors from any step in the chain.
Error Handling in Chains
A single .catch() at the end catches errors from any .then() in the chain. If any step rejects or throws, execution jumps to the nearest .catch():
fetchData('/api/users/999') // User doesn't exist → 404
.then(user => {
console.log('This never runs');
return fetchData('/api/posts');
})
.then(posts => {
console.log('This also never runs');
})
.catch(err => {
console.log('Caught:', err); // "HTTP Error: 404"
})
.finally(() => {
console.log('Cleanup: always runs');
});
Promise.all — Parallel Execution
const p1 = fetchData('/api/users/1');
const p2 = fetchData('/api/users/2');
const p3 = fetchData('/api/users/3');
Promise.all([p1, p2, p3])
.then(function(results) {
console.log('User 1:', results[0].name);
console.log('User 2:', results[1].name);
console.log('User 3:', results[2].name);
})
.catch(function(err) {
console.log('One of the requests failed:', err);
});
Promise.all() takes an array of Promises and runs them all in parallel. It resolves when all of them succeed (returning an array of results) or rejects as soon as any one of them fails.
Promise.race — First to Settle
const fast = new Promise(resolve => setTimeout(() => resolve('Fast'), 1000));
const slow = new Promise(resolve => setTimeout(() => resolve('Slow'), 3000));
Promise.race([fast, slow])
.then(result => console.log(result));
// Output: "Fast" (after 1 second)
Promise.race() resolves or rejects with the result of whichever Promise settles first. It is useful for implementing timeouts.
Promise Utility Methods
| Method | Resolves When | Rejects When |
|---|---|---|
Promise.all() | All Promises fulfill | Any one rejects |
Promise.race() | First Promise settles (win or lose) | First Promise rejects |
Promise.allSettled() | All Promises settle (regardless of outcome) | Never rejects |
Promise.any() | First Promise fulfills | All Promises reject |
Common Mistakes
Forgetting to return in .then()
// WRONG — the chain breaks
fetchUser(1)
.then(user => {
fetchPosts(user.id); // Missing return!
})
.then(posts => {
console.log(posts); // undefined!
});
// CORRECT — return the Promise
fetchUser(1)
.then(user => {
return fetchPosts(user.id); // Return!
})
.then(posts => {
console.log(posts); // Works!
});
Nesting Promises (defeating the purpose)
// WRONG — this is just callback hell with Promises
fetchUser(1).then(user => {
fetchPosts(user.id).then(posts => {
fetchComments(posts[0].id).then(comments => {
console.log(comments);
});
});
});
// CORRECT — flat chain
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => console.log(comments));
Key Takeaways
- A Promise represents a future value that will be either fulfilled or rejected
- Use
.then()for success,.catch()for errors, and.finally()for cleanup - Each
.then()returns a new Promise, enabling flat, readable chains - A single
.catch()at the end handles errors from any step in the chain Promise.all()runs multiple Promises in parallel and waits for all to complete- Always
returninside.then()to keep the chain connected - Promises solve callback hell by replacing deep nesting with flat chains