Promises: Creating & Consuming
Promises: Creating & Consuming
Promises are JavaScript's solution to asynchronous operations — fetching data, reading files, querying databases. Understanding Promises deeply is essential because they underpin async/await, React data fetching, and virtually all modern JavaScript.
What Is a Promise?
A Promise is an object representing an operation that will complete (or fail) in the future. It has three states:
- Pending — operation in progress
- Fulfilled — operation completed successfully (has a value)
- Rejected — operation failed (has a reason/error)
// A Promise is created with a function that receives resolve and reject
const myPromise = new Promise((resolve, reject) => {
// Do async work here...
const success = true;
if (success) {
resolve("Operation succeeded!"); // Fulfills the promise
} else {
reject(new Error("Something went wrong")); // Rejects the promise
}
});
// State transitions:
// pending → fulfilled (when resolve is called)
// pending → rejected (when reject is called)
// Once settled, state never changes
Consuming Promises with .then() / .catch()
fetch("https://api.github.com/users/octocat")
.then(response => response.json()) // .then() runs on success
.then(user => {
console.log(user.name);
console.log(user.followers);
})
.catch(error => { // .catch() runs on any error
console.error("Failed:", error.message);
})
.finally(() => { // .finally() always runs
console.log("Request finished");
});
Promise Chaining
Each .then() returns a new Promise, allowing chains:
function getUser(userId) {
return fetch(`/api/users/${userId}`).then(r => r.json());
}
function getUserPosts(user) {
return fetch(`/api/posts?userId=${user.id}`).then(r => r.json());
}
// Chain — each step gets the result of the previous
getUser(1)
.then(user => {
console.log(`User: ${user.name}`);
return getUserPosts(user); // Return a new Promise
})
.then(posts => {
console.log(`Posts: ${posts.length}`);
})
.catch(error => console.error(error));
Key rule: If you return a value from .then(), the next .then() gets that value. If you return a Promise, the next .then() waits for it to resolve.
Creating Promises for Real Use Cases
// Wrap a callback-based API in a Promise
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (error, data) => {
if (error) reject(error);
else resolve(data);
});
});
}
// Promisify a database query
function queryDB(sql, params) {
return new Promise((resolve, reject) => {
db.query(sql, params, (err, results) => {
if (err) reject(err);
else resolve(results);
});
});
}
Running Promises in Parallel
// Promise.all — run multiple promises concurrently, wait for ALL
const [users, products, orders] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/products').then(r => r.json()),
fetch('/api/orders').then(r => r.json()),
]);
// All three requests run at the same time — much faster than sequential
// If ANY promise rejects, Promise.all rejects immediately
Promise.all([
Promise.resolve(1),
Promise.reject(new Error("Failed!")), // This causes immediate rejection
Promise.resolve(3),
])
.catch(err => console.error(err.message)); // "Failed!"
// Promise.allSettled — wait for ALL, even if some fail
const results = await Promise.allSettled([
fetch('/api/users').then(r => r.json()),
fetch('/api/broken-endpoint').then(r => r.json()),
fetch('/api/products').then(r => r.json()),
]);
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.log('Failed:', result.reason);
}
});
// Use this when you want results from as many requests as possible
// Promise.race — resolves/rejects with the FIRST to settle
// Common use: timeouts
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timed out')), ms)
);
return Promise.race([promise, timeout]);
}
withTimeout(fetch('/api/slow-endpoint'), 5000)
.then(r => r.json())
.catch(err => console.error(err.message)); // "Request timed out" if > 5s
Error Handling with Promises
// Errors propagate through the chain until caught
fetch('/api/users')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`); // Triggers .catch()
}
return response.json();
})
.then(users => {
return users.map(user => processUser(user)); // Error here also goes to .catch()
})
.catch(error => {
// Handles errors from ANY step above
console.error("Something failed:", error.message);
return []; // Return fallback — chain continues with []
})
.then(result => {
console.log("Final result:", result);
});
The Common Pitfall: Forgetting to Return
// BUG: forgetting return in .then()
fetch('/api/user')
.then(response => {
response.json(); // No return! Next .then() gets undefined
})
.then(user => {
console.log(user); // undefined — not the user object!
});
// FIX:
fetch('/api/user')
.then(response => response.json()) // Arrow function implicitly returns
.then(user => {
console.log(user); // The actual user object ✓
});
Promise vs Async/Await
Promises and async/await are the same thing — async/await is just syntax sugar over Promises:
// With Promises
function getUser(id) {
return fetch(`/api/users/${id}`)
.then(r => r.json())
.catch(e => console.error(e));
}
// With async/await (same behavior)
async function getUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
return await response.json();
} catch (e) {
console.error(e);
}
}
async/await reads more like synchronous code and is generally preferred for new code. Understanding Promises first makes async/await obvious.
Next lesson: async/await — writing clean asynchronous code that reads like synchronous code.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises