Promise Basics
A Promise is created with the new Promise(executor) constructor. The executor receives two functions: resolve (call with the success value) and reject (call with an Error).
// Creating a Promise
const promise = new Promise(function(resolve, reject) {
// Simulate async work (e.g., a network request)
setTimeout(function() {
const success = true;
if (success) {
resolve({ id: 1, name: 'Alice' }); // fulfill with a value
} else {
reject(new Error('Failed to fetch user')); // reject with an Error
}
}, 1000);
});
// Using the Promise
promise
.then(user => console.log('User:', user.name)) // 'Alice'
.catch(err => console.error('Error:', err.message));
// Promise.resolve / Promise.reject - create already-settled Promises
const resolved = Promise.resolve(42); // instantly fulfilled
const rejected = Promise.reject(new Error('oops')); // instantly rejected
resolved.then(val => console.log(val)); // 42
rejected.catch(err => console.log(err.message)); // 'oops'
.then, .catch, .finally
Each handler returns a new Promise, which is what enables chaining.
fetchUser(42)
.then(function(user) {
// Called if fetchUser resolves
console.log('Got user:', user.name);
return user; // passed to next .then
})
.catch(function(err) {
// Called if fetchUser rejects OR if any prior .then throws
console.error('Something failed:', err.message);
// return a default to recover, or re-throw to keep failing
return { name: 'Guest' }; // recover with default
})
.finally(function() {
// Always runs - whether resolved or rejected
// Great for cleanup: hiding spinners, resetting state
// Does NOT receive the value - use .then for that
hideLoadingSpinner();
});
// .then(onFulfilled, onRejected) - two-argument form (less common)
fetchUser(42).then(
user => console.log('success:', user.name),
err => console.error('error:', err.message)
);
// Difference from .catch: this form does NOT catch errors thrown inside onFulfilled
Chaining Promises
Return a Promise inside .then() to wait for it before calling the next .then(). This creates sequential async steps in a flat chain.
// Sequential async steps - flat chain
getUser(userId)
.then(user => getOrders(user.id)) // return a new Promise
.then(orders => getDetails(orders[0])) // wait for previous
.then(detail => render(detail))
.catch(err => showError(err.message)); // catches any error in the chain
// Transforming values inline (no new Promise needed for sync transforms)
fetch('/api/products')
.then(response => response.json()) // returns Promise
.then(data => data.filter(p => p.inStock)) // sync transform
.then(inStock => {
inStock.forEach(p => renderProduct(p));
return inStock.length; // pass count to next handler
})
.then(count => console.log(`${count} products shown`))
.catch(err => console.error(err));
// WRONG - nested .then recreates callback hell
fetch('/api/user').then(res => {
res.json().then(user => { // nested - don't do this
fetch('/api/orders/' + user.id).then(r => {
// ever deeper nesting...
});
});
});
// RIGHT - return the inner Promise so the chain stays flat
fetch('/api/user')
.then(res => res.json())
.then(user => fetch('/api/orders/' + user.id))
.then(res => res.json())
.then(orders => console.log(orders))
.catch(err => console.error(err));
Promise States
| State | Description | Transitions to |
|---|---|---|
| pending | Initial state - operation in progress | fulfilled or rejected |
| fulfilled | Operation completed successfully | Settled - cannot change again |
| rejected | Operation failed | Settled - cannot change again |
Once a Promise resolves or rejects, its state and value are locked forever. Calling resolve() a second time has no effect. This is a key difference from callbacks, which can theoretically be called multiple times.
Promise.all
Run multiple async operations in parallel and wait for all of them to finish. Fails fast - if any promise rejects, the whole thing rejects.
// Run three fetches in parallel - much faster than sequential
Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json()),
]).then(([user, posts, comments]) => {
// All three resolved - destructure the results array
console.log(user, posts, comments);
}).catch(err => {
// Any single rejection causes this to fire
console.error('At least one request failed:', err);
});
// With async/await (cleaner)
async function loadDashboard() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
render(user, posts, comments);
}
// Dynamic array of promises
const userIds = [1, 2, 3, 4, 5];
const userPromises = userIds.map(id => fetchUser(id));
const users = await Promise.all(userPromises); // array of 5 users
Promise Combinators
// Promise.allSettled - wait for ALL, regardless of failures
// Returns: [{ status: 'fulfilled', value: ... }, { status: 'rejected', reason: ... }]
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(2), // might fail
fetchUser(3),
]);
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Got user:', result.value.name);
} else {
console.error('Failed:', result.reason.message);
}
});
// Promise.race - resolves/rejects with the FIRST settled Promise
// Use case: timeout pattern
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
return Promise.race([promise, timeout]);
}
const user = await withTimeout(fetchUser(1), 5000);
// Promise.any - resolves with the FIRST fulfilled Promise (ignores rejections)
// Rejects with AggregateError only if ALL reject
const fastest = await Promise.any([
fetch('https://cdn1.example.com/data.json'),
fetch('https://cdn2.example.com/data.json'),
fetch('https://cdn3.example.com/data.json'),
]).then(r => r.json());
// Summary: which combinator to use?
// Promise.all - need ALL results, fail fast if any fails
// Promise.allSettled - need ALL results, handle each failure individually
// Promise.race - need FIRST settled (fulfilled or rejected)
// Promise.any - need FIRST fulfilled (timeout if all fail)
Creating Promises
// Wrap setTimeout in a Promise
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
await delay(1000); // pause execution for 1 second
// Wrap an event in a Promise
function waitForClick(element) {
return new Promise(resolve => {
element.addEventListener('click', resolve, { once: true });
});
}
const event = await waitForClick(document.querySelector('#btn'));
// Wrap a callback-based API
function readFileAsync(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// Conditional Promise - sometimes async, sometimes not
function getUser(id) {
if (cache.has(id)) {
return Promise.resolve(cache.get(id)); // synchronously resolved
}
return fetchUser(id).then(user => {
cache.set(id, user);
return user;
});
}
Common Mistakes
// Mistake 1: Forgetting to return a Promise inside .then()
fetchUser(1)
.then(user => {
fetchOrders(user.id); // forgot return! next .then gets undefined
})
.then(orders => console.log(orders)); // undefined - bug!
// Fix:
fetchUser(1)
.then(user => fetchOrders(user.id)) // return the Promise
.then(orders => console.log(orders));
// Mistake 2: Unhandled rejections
fetchUser(1)
.then(user => console.log(user));
// No .catch() - rejection is silently swallowed (or causes UnhandledPromiseRejection)
// Fix: always add .catch()
fetchUser(1)
.then(user => console.log(user))
.catch(err => console.error(err));
// Mistake 3: Promise constructor anti-pattern
// WRONG - unnecessary Promise wrapping of another Promise
function getUser(id) {
return new Promise((resolve, reject) => {
fetch('/api/users/' + id)
.then(r => r.json())
.then(resolve)
.catch(reject);
});
}
// RIGHT - just return the Promise chain directly
function getUser(id) {
return fetch('/api/users/' + id).then(r => r.json());
}
Promise chains are powerful but async/await (syntactic sugar over Promises) is often clearer for sequential async code. The next lesson covers async/await in depth. Both compile to the same Promise machinery - choose whichever reads better for the situation.