JavaScript Promises: The Ultimate Guide
Master JavaScript Promises from basics to advanced patterns. Learn Promise creation, chaining, error handling, and real-world applications.
JavaScript Promises: The Ultimate Guide
Promises are a fundamental feature in modern JavaScript for handling asynchronous operations. They provide a cleaner alternative to callbacks and form the foundation for async/await syntax.
What is a Promise?
A Promise is an object representing the eventual completion or failure of an asynchronous operation. Think of it as a placeholder for a value that will be available in the future.
// Real-world analogy
// Promise = "I promise to return your book"
// Pending = "I still have your book"
// Fulfilled = "Here's your book back!"
// Rejected = "Sorry, I lost your book"
const promise = new Promise((resolve, reject) => {
// Asynchronous operation
setTimeout(() => {
const success = true;
if (success) {
resolve('Operation completed!');
} else {
reject('Operation failed!');
}
}, 1000);
});
Creating Promises
Basic Promise Constructor
const myPromise = new Promise((resolve, reject) => {
// The executor function runs immediately
console.log('Promise executor running');
// Simulate async operation
setTimeout(() => {
const randomNumber = Math.random();
if (randomNumber > 0.5) {
resolve(randomNumber); // Success
} else {
reject(new Error('Number too small')); // Failure
}
}, 1000);
});
// Using the promise
myPromise
.then((value) => console.log('Success:', value))
.catch((error) => console.error('Error:', error));
Promise States
A Promise can be in one of three states:
// 1. Pending - initial state
const pendingPromise = new Promise((resolve) => {
// Never resolves or rejects
});
console.log(pendingPromise); // Promise {<pending>}
// 2. Fulfilled - operation completed successfully
const fulfilledPromise = Promise.resolve('Success!');
console.log(fulfilledPromise); // Promise {<fulfilled>: "Success!"}
// 3. Rejected - operation failed
const rejectedPromise = Promise.reject('Failure!');
console.log(rejectedPromise); // Promise {<rejected>: "Failure!"}
Promise Methods
then() and catch()
// Basic usage
promise
.then((result) => {
console.log('Success:', result);
return result * 2; // Return value passed to next then
})
.then((doubled) => {
console.log('Doubled:', doubled);
})
.catch((error) => {
console.error('Error:', error);
})
.finally(() => {
console.log('Cleanup operations');
});
// Multiple handlers
const promise = Promise.resolve(42);
// Each gets the same value
promise.then((x) => console.log('First:', x));
promise.then((x) => console.log('Second:', x));
promise.then((x) => console.log('Third:', x));
Promise Chaining
// Sequential operations
function fetchUser(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: 'John Doe' });
}, 1000);
});
}
function fetchPosts(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, userId, title: 'First Post' },
{ id: 2, userId, title: 'Second Post' },
]);
}, 1000);
});
}
function fetchComments(postId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, postId, text: 'Great post!' },
{ id: 2, postId, text: 'Thanks for sharing!' },
]);
}, 1000);
});
}
// Chain promises
fetchUser(1)
.then((user) => {
console.log('User:', user);
return fetchPosts(user.id);
})
.then((posts) => {
console.log('Posts:', posts);
return fetchComments(posts[0].id);
})
.then((comments) => {
console.log('Comments:', comments);
})
.catch((error) => {
console.error('Error in chain:', error);
});
Error Handling
Catching Errors
// Errors propagate through the chain
Promise.resolve('Start')
.then((value) => {
console.log(value);
throw new Error('Something went wrong!');
})
.then((value) => {
// This is skipped
console.log("This won't run");
})
.catch((error) => {
console.error('Caught:', error.message);
return 'Recovered'; // Can recover from error
})
.then((value) => {
console.log('Continuing with:', value); // "Recovered"
});
// Multiple catch blocks
promise
.then(handleSuccess)
.catch(handleError)
.then(processResult)
.catch(handleProcessingError);
Error Types
// Different error scenarios
function riskyOperation() {
return new Promise((resolve, reject) => {
const random = Math.random();
if (random < 0.33) {
reject(new Error('General error'));
} else if (random < 0.66) {
reject(new TypeError('Type error'));
} else {
resolve('Success');
}
});
}
riskyOperation()
.then((result) => console.log(result))
.catch((error) => {
if (error instanceof TypeError) {
console.error('Type error occurred:', error);
} else if (error instanceof Error) {
console.error('General error:', error);
} else {
console.error('Unknown error:', error);
}
});
Static Promise Methods
Promise.resolve() and Promise.reject()
// Creating resolved promises
const resolved1 = Promise.resolve(42);
const resolved2 = Promise.resolve({ name: 'John' });
// Promise.resolve with a promise returns the same promise
const existingPromise = new Promise((r) => r('Hello'));
const wrapped = Promise.resolve(existingPromise);
console.log(wrapped === existingPromise); // true
// Creating rejected promises
const rejected = Promise.reject(new Error('Failed'));
// Useful for starting chains
Promise.resolve()
.then(() => fetchData())
.then((data) => processData(data))
.catch(handleError);
Promise.all()
Waits for all promises to fulfill or any to reject:
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3]).then((results) => {
console.log(results); // [1, 2, 3]
});
// Real-world example
async function fetchUserData(userId) {
const promises = [
fetchUser(userId),
fetchPosts(userId),
fetchFriends(userId),
];
try {
const [user, posts, friends] = await Promise.all(promises);
return { user, posts, friends };
} catch (error) {
console.error('Failed to fetch user data:', error);
}
}
// Fails fast - if any promise rejects
Promise.all([
Promise.resolve(1),
Promise.reject(new Error('Failed')),
Promise.resolve(3),
]).catch((error) => {
console.error(error.message); // "Failed"
});
Promise.allSettled()
Waits for all promises to settle (fulfill or reject):
const promises = [
Promise.resolve(1),
Promise.reject(new Error('Failed')),
Promise.resolve(3),
];
Promise.allSettled(promises).then((results) => {
results.forEach((result) => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.log('Failed:', result.reason);
}
});
});
// Output:
// Success: 1
// Failed: Error: Failed
// Success: 3
// Practical use case
async function batchOperation(items) {
const operations = items.map((item) =>
processItem(item).catch((err) => ({ error: err, item }))
);
const results = await Promise.allSettled(operations);
const successful = results
.filter((r) => r.status === 'fulfilled')
.map((r) => r.value);
const failed = results
.filter((r) => r.status === 'rejected')
.map((r) => r.reason);
return { successful, failed };
}
Promise.race()
Resolves/rejects with the first promise to settle:
const slow = new Promise((resolve) => setTimeout(() => resolve('slow'), 2000));
const fast = new Promise((resolve) => setTimeout(() => resolve('fast'), 1000));
const faster = new Promise((resolve) =>
setTimeout(() => resolve('faster'), 500)
);
Promise.race([slow, fast, faster]).then((winner) => {
console.log(winner); // "faster"
});
// Timeout implementation
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
}
function fetchWithTimeout(url, ms = 5000) {
return Promise.race([fetch(url), timeout(ms)]);
}
// First to fail also wins
Promise.race([
new Promise((_, reject) => setTimeout(() => reject('Error'), 1000)),
new Promise((resolve) => setTimeout(() => resolve('Success'), 2000)),
]).catch((error) => {
console.log('First to settle was rejection:', error); // "Error"
});
Promise.any()
Resolves with the first fulfilled promise:
const promises = [
Promise.reject('Error 1'),
Promise.reject('Error 2'),
Promise.resolve('Success!'),
Promise.resolve('Also success'),
];
Promise.any(promises)
.then((value) => {
console.log(value); // "Success!" (first to fulfill)
})
.catch((error) => {
console.log('All promises rejected:', error);
});
// All rejected scenario
Promise.any([
Promise.reject('Error 1'),
Promise.reject('Error 2'),
Promise.reject('Error 3'),
]).catch((error) => {
console.log(error); // AggregateError: All promises were rejected
console.log(error.errors); // ["Error 1", "Error 2", "Error 3"]
});
Converting Callbacks to Promises
Promisifying Functions
// Original callback-based function
function readFileCallback(filename, callback) {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) callback(err, null);
else callback(null, data);
});
}
// Convert to Promise
function readFilePromise(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// Generic promisify function
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
};
}
// Usage
const readFile = promisify(fs.readFile);
readFile('file.txt', 'utf8')
.then((data) => console.log(data))
.catch((err) => console.error(err));
Working with Event Emitters
function waitForEvent(emitter, eventName) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`Timeout waiting for ${eventName}`));
}, 5000);
emitter.once(eventName, (data) => {
clearTimeout(timeout);
resolve(data);
});
emitter.once('error', (error) => {
clearTimeout(timeout);
reject(error);
});
});
}
// Usage
const server = createServer();
waitForEvent(server, 'listening')
.then(() => console.log('Server is ready'))
.catch((err) => console.error('Server failed to start:', err));
Advanced Promise Patterns
Sequential Execution
// Process array items sequentially
async function processSequentially(items, processor) {
const results = [];
for (const item of items) {
const result = await processor(item);
results.push(result);
}
return results;
}
// Using reduce for sequential promises
function processSequentiallyReduce(items, processor) {
return items.reduce((promise, item) => {
return promise.then((results) =>
processor(item).then((result) => [...results, result])
);
}, Promise.resolve([]));
}
Batch Processing
async function processBatches(items, batchSize, processor) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map((item) => processor(item))
);
results.push(...batchResults);
}
return results;
}
// With concurrency limit
class PromisePool {
constructor(concurrency) {
this.concurrency = concurrency;
this.current = 0;
this.queue = [];
}
async run(fn) {
while (this.current >= this.concurrency) {
await new Promise((resolve) => this.queue.push(resolve));
}
this.current++;
try {
return await fn();
} finally {
this.current--;
const next = this.queue.shift();
if (next) next();
}
}
}
Memoization
function memoizePromise(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const promise = fn
.apply(this, args)
.then((result) => {
cache.set(key, Promise.resolve(result));
return result;
})
.catch((error) => {
cache.delete(key); // Remove failed promises
throw error;
});
cache.set(key, promise);
return promise;
};
}
// Usage
const memoizedFetch = memoizePromise(fetch);
// First call makes the request
memoizedFetch('/api/data').then(handleData);
// Subsequent calls return cached promise
memoizedFetch('/api/data').then(handleData); // No new request
Circuit Breaker Pattern
class CircuitBreaker {
constructor(fn, threshold = 5, timeout = 60000) {
this.fn = fn;
this.threshold = threshold;
this.timeout = timeout;
this.failures = 0;
this.nextTry = Date.now();
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}
async call(...args) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextTry) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await this.fn(...args);
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failures++;
if (this.failures >= this.threshold) {
this.state = 'OPEN';
this.nextTry = Date.now() + this.timeout;
}
}
}
Promise Anti-Patterns
The Explicit Construction Anti-Pattern
// Bad - unnecessary promise construction
function bad() {
return new Promise((resolve, reject) => {
fetchData()
.then((data) => resolve(data))
.catch((err) => reject(err));
});
}
// Good - return the promise directly
function good() {
return fetchData();
}
// Bad - wrapping synchronous code
function badSync() {
return new Promise((resolve) => {
resolve(42);
});
}
// Good - use Promise.resolve
function goodSync() {
return Promise.resolve(42);
}
Forgetting to Return
// Bad - broken chain
promise
.then((data) => {
processData(data); // Missing return
})
.then((result) => {
console.log(result); // undefined
});
// Good
promise
.then((data) => {
return processData(data);
})
.then((result) => {
console.log(result);
});
Nested Promises
// Bad - promise hell
getData().then((data) => {
return processData(data).then((processed) => {
return saveData(processed).then((saved) => {
return notifyUser(saved);
});
});
});
// Good - flat chain
getData()
.then((data) => processData(data))
.then((processed) => saveData(processed))
.then((saved) => notifyUser(saved));
// Better - async/await
async function handleData() {
const data = await getData();
const processed = await processData(data);
const saved = await saveData(processed);
return notifyUser(saved);
}
Testing Promises
// Using async/await in tests
describe('Promise tests', () => {
test('should resolve with correct value', async () => {
const result = await Promise.resolve(42);
expect(result).toBe(42);
});
test('should reject with error', async () => {
await expect(Promise.reject(new Error('Failed'))).rejects.toThrow('Failed');
});
test('should handle async operations', async () => {
const mockFetch = jest.fn().mockResolvedValue({ data: 'test' });
const result = await mockFetch();
expect(result.data).toBe('test');
});
});
// Testing with done callback
test('promise resolves', (done) => {
Promise.resolve('success').then((value) => {
expect(value).toBe('success');
done();
});
});
Best Practices
- Always handle errors - Add
.catch()
or try-catch - Return promises from
.then()
- Maintain the chain - Use
Promise.all()
for parallel operations - Avoid the explicit construction anti-pattern
- Consider async/await for complex flows
- Add timeouts for external operations
- Clean up resources in
finally
- Use proper error types and messages
Conclusion
Promises are a powerful tool for handling asynchronous operations in JavaScript. Key takeaways:
- Promises represent future values
- Three states: pending, fulfilled, rejected
- Chain operations with
.then()
and handle errors with.catch()
- Static methods like
Promise.all()
enable complex async patterns - Avoid common anti-patterns for cleaner code
- Modern async/await syntax builds on Promises
Master Promises to write more maintainable asynchronous JavaScript code!