Promises
Table of Contents
JavaScript is single-threaded — it can only do one thing at a time. But web applications need to fetch data from servers, read files, and wait for user input without freezing the page. Promises are how JavaScript handles these asynchronous operations: they represent a value that doesn’t exist yet but will (or won’t) in the future.
The Problem Promises Solve
Before promises, async code used callbacks. This worked for simple cases but quickly became unmanageable:
// "Callback hell" — nested callbacks are hard to read and maintain
getUser(userId, function (user) {
getOrders(user.id, function (orders) {
getOrderDetails(orders[0].id, function (details) {
getShippingStatus(details.trackingId, function (status) {
console.log(status);
// Error handling? Good luck.
});
});
});
});
Promises flatten this into a readable chain.
What Is a Promise?
A promise is an object that represents the eventual result of an async operation. It’s in one of three states:
- Pending — the operation hasn’t completed yet
- Fulfilled — the operation succeeded, and the promise has a value
- Rejected — the operation failed, and the promise has a reason (error)
Once a promise is fulfilled or rejected, it’s settled — it can’t change state again.
Using Promises
Most of the time, you’ll consume promises returned by APIs (like fetch), not create them yourself.
.then() and .catch()
fetch("https://api.example.com/users/1")
.then(response => response.json())
.then(user => {
console.log(user.name);
})
.catch(error => {
console.error("Failed to fetch user:", error.message);
});
.then() runs when the promise fulfills. .catch() runs when it rejects. They return new promises, so you can chain them.
Chaining
Each .then() returns a new promise, so you can chain operations sequentially:
fetch("/api/user")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(user => fetch(`/api/orders?userId=${user.id}`))
.then(response => response.json())
.then(orders => {
console.log(`User has ${orders.length} orders`);
})
.catch(error => {
// Catches errors from ANY step in the chain
console.error("Something went wrong:", error.message);
});
.finally()
Runs regardless of whether the promise fulfilled or rejected — useful for cleanup:
const spinner = document.querySelector(".spinner");
spinner.style.display = "block";
fetch("/api/data")
.then(response => response.json())
.then(data => renderData(data))
.catch(error => showError(error))
.finally(() => {
spinner.style.display = "none"; // always hide spinner
});
Creating Promises
When you need to wrap callback-based code or create your own async operations:
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
delay(2000).then(() => console.log("2 seconds passed"));
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Failed to load: ${url}`));
img.src = url;
});
}
loadImage("/photo.jpg")
.then(img => document.body.appendChild(img))
.catch(err => console.error(err.message));
The Promise constructor takes a function with two parameters:
resolve(value)— call this when the operation succeedsreject(error)— call this when it fails
Promise Static Methods
Promise.all — wait for all to complete
const urls = ["/api/users", "/api/posts", "/api/comments"];
const requests = urls.map(url => fetch(url).then(r => r.json()));
Promise.all(requests)
.then(([users, posts, comments]) => {
console.log(`${users.length} users`);
console.log(`${posts.length} posts`);
console.log(`${comments.length} comments`);
})
.catch(error => {
// If ANY request fails, the whole thing rejects
console.error("One of the requests failed:", error);
});
Promise.allSettled — wait for all, regardless of success/failure
const requests = [
fetch("/api/users"),
fetch("/api/broken-endpoint"),
fetch("/api/posts")
];
Promise.allSettled(requests).then(results => {
results.forEach((result, i) => {
if (result.status === "fulfilled") {
console.log(`Request ${i} succeeded`);
} else {
console.log(`Request ${i} failed: ${result.reason}`);
}
});
});
Promise.race — first one to settle wins
// Timeout pattern: race a fetch against a timer
function fetchWithTimeout(url, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Request timed out")), ms);
});
return Promise.race([fetch(url), timeout]);
}
fetchWithTimeout("/api/slow-endpoint", 5000)
.then(response => response.json())
.catch(error => console.error(error.message));
Promise.any — first one to fulfill wins
// Try multiple CDNs, use whichever responds first
Promise.any([
fetch("https://cdn1.example.com/data.json"),
fetch("https://cdn2.example.com/data.json"),
fetch("https://cdn3.example.com/data.json")
])
.then(response => response.json())
.then(data => console.log("Got data from fastest CDN"))
.catch(error => console.error("All CDNs failed"));
Error Handling Patterns
Always catch errors
An unhandled promise rejection is a bug. Always add .catch() at the end of your chains:
// Bad — unhandled rejection if fetch fails
fetch("/api/data").then(r => r.json()).then(processData);
// Good
fetch("/api/data")
.then(r => r.json())
.then(processData)
.catch(handleError);
Recovering from errors
.catch() returns a new promise, so you can recover and continue the chain:
fetch("/api/user-preferences")
.then(r => r.json())
.catch(() => {
// If preferences fail to load, use defaults
return { theme: "light", fontSize: 14 };
})
.then(prefs => applyPreferences(prefs));
Retrying failed requests
function fetchWithRetry(url, retries = 3) {
return fetch(url).catch(error => {
if (retries > 0) {
console.log(`Retrying... (${retries} attempts left)`);
return fetchWithRetry(url, retries - 1);
}
throw error;
});
}
Common Mistakes
Forgetting to return in .then()
// Bug — the second .then() receives undefined
fetch("/api/user")
.then(response => {
response.json(); // missing return!
})
.then(user => {
console.log(user); // undefined
});
// Fix
fetch("/api/user")
.then(response => response.json())
.then(user => console.log(user));
Creating promises unnecessarily
// Bad — wrapping an existing promise in a new one
function getUser(id) {
return new Promise((resolve, reject) => {
fetch(`/api/users/${id}`)
.then(r => r.json())
.then(resolve)
.catch(reject);
});
}
// Good — just return the promise chain
function getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
What’s Next
Promises are powerful, but chaining .then() calls can still get verbose. Async/await builds on promises to let you write asynchronous code that looks synchronous — it’s the modern way to handle async operations in JavaScript.