Promises
Promises provide a cleaner way to handle asynchronous operations compared to callbacks. They represent a value that may be available now, in the future, or never. Understanding Promises is essential for modern JavaScript and Node.js development.
What is a Promise?
A Promise is an object representing the eventual completion or failure of an asynchronous operation:
const promise = new Promise((resolve, reject) => {
// Asynchronous operation
setTimeout(() => {
const success = true;
if (success) {
resolve('Operation completed!');
} else {
reject(new Error('Operation failed'));
}
}, 1000);
});
promise
.then(result => console.log(result))
.catch(error => console.error(error));
Promise States
A Promise can be in one of three states:
- Pending: Initial state, operation in progress
- Fulfilled: Operation completed successfully
- Rejected: Operation failed
Once a Promise is fulfilled or rejected, it's "settled" and cannot change state.
// Pending
const pending = new Promise(() => {});
console.log(pending); // Promise { <pending> }
// Fulfilled
const fulfilled = Promise.resolve('done');
console.log(fulfilled); // Promise { 'done' }
// Rejected
const rejected = Promise.reject(new Error('failed'));
console.log(rejected); // Promise { <rejected> Error: failed }
then() and catch()
Use .then() to handle successful results and .catch() to handle errors:
fetchData()
.then(data => {
console.log('Data:', data);
return processData(data);
})
.then(processed => {
console.log('Processed:', processed);
})
.catch(error => {
console.error('Error:', error.message);
});
Chaining Promises
Each .then() returns a new Promise, allowing you to chain operations:
getUser(1)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => displayDetails(details))
.catch(error => handleError(error));
Compare this to callback hell:
// Callbacks (messy)
getUser(1, (err, user) => {
getOrders(user.id, (err, orders) => {
getOrderDetails(orders[0].id, (err, details) => {
displayDetails(details);
});
});
});
// Promises (clean)
getUser(1)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => displayDetails(details));
Returning Values in then()
Whatever you return from .then() becomes the value for the next .then():
Promise.resolve(5)
.then(x => x * 2) // Returns 10
.then(x => x + 3) // Returns 13
.then(x => console.log(x)); // Logs 13
// Returning a Promise
Promise.resolve(5)
.then(x => Promise.resolve(x * 2)) // Returns Promise
.then(x => console.log(x)); // Logs 10 (auto-unwrapped)
Error Handling
Errors in any part of the chain are caught by .catch():
fetchUser(999)
.then(user => fetchOrders(user.id)) // This might fail
.then(orders => processOrders(orders)) // Or this
.then(result => saveResult(result)) // Or this
.catch(error => {
// Catches any error from the chain
console.error('Something failed:', error.message);
});
finally()
Use .finally() for cleanup that should run regardless of success or failure:
showLoadingSpinner();
fetchData()
.then(data => displayData(data))
.catch(error => showError(error))
.finally(() => {
hideLoadingSpinner(); // Always runs
});
Creating Promises
Promise.resolve() and Promise.reject()
Quickly create settled Promises:
// Immediately resolved
const resolved = Promise.resolve('done');
// Immediately rejected
const rejected = Promise.reject(new Error('failed'));
// Useful for starting chains
Promise.resolve(data)
.then(processStep1)
.then(processStep2);
The Promise Constructor
Create custom async operations:
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
function readFilePromise(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
Promise.all()
Wait for multiple Promises to complete:
const promise1 = fetchUser(1);
const promise2 = fetchUser(2);
const promise3 = fetchUser(3);
Promise.all([promise1, promise2, promise3])
.then(users => {
console.log('All users:', users);
})
.catch(error => {
console.log('One failed:', error);
});
If any Promise rejects, the whole Promise.all rejects.
Promise.race()
Returns when the first Promise settles:
const slow = new Promise(resolve =>
setTimeout(() => resolve('slow'), 2000)
);
const fast = new Promise(resolve =>
setTimeout(() => resolve('fast'), 500)
);
Promise.race([slow, fast])
.then(result => console.log(result)); // 'fast'
Useful for timeouts:
function fetchWithTimeout(url, timeout) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
);
return Promise.race([fetchPromise, timeoutPromise]);
}
Promise.allSettled()
Wait for all Promises to settle, regardless of success or failure:
const promises = [
Promise.resolve('success'),
Promise.reject(new Error('failed')),
Promise.resolve('another success')
];
Promise.allSettled(promises)
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Value:', result.value);
} else {
console.log('Error:', result.reason.message);
}
});
});
Converting Callbacks to Promises
Wrap callback-based APIs in Promises:
const fs = require('fs');
// Manual promisification
function readFilePromise(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// Using util.promisify (built-in)
const util = require('util');
const readFile = util.promisify(fs.readFile);
// Usage
readFile('file.txt', 'utf8')
.then(data => console.log(data))
.catch(err => console.error(err));
Common Promise Patterns
Sequential Execution
// Process items one at a time
async function processSequentially(items) {
let results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
}
return results;
}
// Using reduce
function processSequentiallyWithReduce(items) {
return items.reduce((promise, item) => {
return promise.then(results =>
processItem(item).then(result => [...results, result])
);
}, Promise.resolve([]));
}
Retry Pattern
function retry(fn, retries = 3, delay = 1000) {
return fn().catch(err => {
if (retries === 0) throw err;
return new Promise(resolve =>
setTimeout(() => resolve(retry(fn, retries - 1, delay)), delay)
);
});
}
// Usage
retry(() => fetchData(url), 3, 1000)
.then(data => console.log(data))
.catch(err => console.error('All retries failed'));
Key Takeaways
- Promises represent eventual completion or failure of async operations
- Use
.then()for success,.catch()for errors,.finally()for cleanup - Chain Promises to avoid callback hell
Promise.all()runs Promises in parallelPromise.race()returns the first settled PromisePromise.allSettled()waits for all, regardless of outcome- Use
Promise.resolve()andPromise.reject()for immediate values - Convert callbacks to Promises with
new Promise()orutil.promisify
Summary
Promises provide a powerful and cleaner alternative to callbacks for handling asynchronous operations. You've learned how to create, chain, and combine Promises, handle errors gracefully, and convert callback-based APIs to Promises. This foundation is essential for understanding async/await, which you'll learn next.

