JavaScript Promises provide a powerful way to handle asynchronous operations, allowing you to write cleaner, more manageable code for tasks like API calls, file operations, and timers. Promises represent the eventual completion (or failure) of an asynchronous operation.
In this tutorial, we'll explore Promise fundamentals, states, methods, chaining, error handling, and best practices for working with asynchronous JavaScript code.
Understanding Promises
A Promise is an object representing the eventual completion or failure of an asynchronous operation.
Promise States
// Promise has three states:
// 1. Pending: Initial state, not yet fulfilled or rejected
// 2. Fulfilled: Operation completed successfully
// 3. Rejected: Operation failed
const promise = new Promise((resolve, reject) => {
// Asynchronous operation
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('Success!'); // Fulfilled
} else {
reject('Error!'); // Rejected
}
}, 1000);
});
console.log(promise); // Promise<pending>
Creating Promises
// Basic Promise constructor
const myPromise = new Promise((resolve, reject) => {
// Executor function
// resolve(value) - fulfill the promise
// reject(reason) - reject the promise
});
// Example: Fetch data
function fetchData(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then(response => {
if (response.ok) {
resolve(response.json());
} else {
reject(new Error(`HTTP error! status: ${response.status}`));
}
})
.catch(error => reject(error));
});
}
// Example: Timer with Promise
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Example: File read with Promise
function readFile(filename) {
return new Promise((resolve, reject) => {
const fs = require('fs');
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
Consuming Promises
// Using then() and catch()
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('Success!'), 1000);
});
promise
.then(result => {
console.log('Fulfilled:', result);
return 'Processed result';
})
.then(processedResult => {
console.log('Processed:', processedResult);
})
.catch(error => {
console.error('Rejected:', error);
})
.finally(() => {
console.log('Promise settled (fulfilled or rejected)');
});
// Chaining with return values
function fetchUser(id) {
return new Promise(resolve => {
setTimeout(() => resolve({ id, name: 'John' }), 1000);
});
}
function fetchUserPosts(userId) {
return new Promise(resolve => {
setTimeout(() => resolve(['Post 1', 'Post 2']), 1000);
});
}
fetchUser(1)
.then(user => {
console.log('User:', user);
return fetchUserPosts(user.id);
})
.then(posts => {
console.log('Posts:', posts);
})
.catch(error => {
console.error('Error:', error);
});
Promise Methods
Built-in Promise methods for handling multiple promises and various scenarios.
Promise.all
// Promise.all - all promises must fulfill
const promise1 = Promise.resolve(3);
const promise2 = new Promise(resolve => setTimeout(resolve, 1000, 'foo'));
const promise3 = Promise.resolve(42);
Promise.all([promise1, promise2, promise3])
.then(values => {
console.log(values); // [3, 'foo', 42]
})
.catch(error => {
console.error('One promise rejected:', error);
});
// Real-world example
function fetchUserData(userId) {
const userPromise = fetchUser(userId);
const postsPromise = fetchUserPosts(userId);
const commentsPromise = fetchUserComments(userId);
return Promise.all([userPromise, postsPromise, commentsPromise])
.then(([user, posts, comments]) => {
return { user, posts, comments };
});
}
// Error handling with Promise.all
const promises = [
Promise.resolve('Success'),
Promise.reject('Error'),
Promise.resolve('Another success')
];
Promise.all(promises)
.then(results => console.log(results))
.catch(error => console.error('Failed:', error)); // "Error"
Promise.allSettled
// Promise.allSettled - wait for all promises to settle
const promises = [
Promise.resolve('Success'),
Promise.reject('Error'),
Promise.resolve('Another success')
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index}:`, result.value);
} else {
console.log(`Promise ${index} failed:`, result.reason);
}
});
});
// Process results based on status
function processBatch(items) {
const promises = items.map(item => processItem(item));
return Promise.allSettled(promises)
.then(results => {
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
// Promise.race - first promise to settle wins
const promise1 = new Promise(resolve => setTimeout(resolve, 500, 'First'));
const promise2 = new Promise(resolve => setTimeout(resolve, 100, 'Second'));
Promise.race([promise1, promise2])
.then(result => console.log('Winner:', result)); // "Second"
// Timeout example
function fetchWithTimeout(url, timeout = 5000) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timeout')), timeout);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
// Race between multiple sources
function fetchFromAnySource(sources) {
const promises = sources.map(source => fetch(source));
return Promise.race(promises);
}
Promise.any
// Promise.any - first fulfilled promise wins (ES2021)
const promises = [
Promise.reject('Error 1'),
Promise.reject('Error 2'),
Promise.resolve('Success'),
Promise.reject('Error 3')
];
Promise.any(promises)
.then(result => console.log('Success:', result)) // "Success"
.catch(error => console.error('All failed:', error));
// Try multiple sources until one succeeds
function fetchFromMultipleSources(urls) {
const promises = urls.map(url => fetch(url).then(r => r.json()));
return Promise.any(promises);
}
// AggregateError when all promises reject
Promise.any([
Promise.reject('Error 1'),
Promise.reject('Error 2')
]).catch(error => {
console.log(error instanceof AggregateError); // true
console.log(error.errors); // ['Error 1', 'Error 2']
});
Error Handling
Proper error handling is crucial for robust Promise-based code.
Catch Method
// Basic error handling
fetchData('/api/data')
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// Multiple catch blocks
fetchData('/api/data')
.then(data => processData(data))
.then(processed => saveData(processed))
.catch(error => {
if (error instanceof NetworkError) {
console.error('Network error:', error);
} else if (error instanceof ValidationError) {
console.error('Validation error:', error);
} else {
console.error('Unknown error:', error);
}
});
// Catch after multiple then chains
promise
.then(result1 => {
return process1(result1);
})
.then(result2 => {
return process2(result2);
})
.then(result3 => {
return process3(result3);
})
.catch(error => {
// Catches errors from any of the above operations
console.error('Error in chain:', error);
});
Error Propagation
// Errors propagate down the chain
function step1() {
return Promise.reject('Error from step1');
}
function step2() {
return Promise.resolve('Success from step2');
}
step1()
.then(result => {
console.log('This won\'t run');
return step2();
})
.then(result => {
console.log('This won\'t run either');
})
.catch(error => {
console.log('Caught:', error); // "Error from step1"
});
// Throwing errors in then handlers
Promise.resolve('data')
.then(data => {
throw new Error('Something went wrong');
})
.catch(error => {
console.error('Caught thrown error:', error);
});
// Returning rejected promises
Promise.resolve('data')
.then(data => {
return Promise.reject('Intentional rejection');
})
.catch(error => {
console.error('Caught rejection:', error);
});
Finally Block
// Finally always runs
fetchData('/api/data')
.then(data => console.log('Data:', data))
.catch(error => console.error('Error:', error))
.finally(() => {
console.log('Cleanup: Always runs');
// Hide loading spinner, close connections, etc.
});
// Finally for resource cleanup
function processWithResource() {
const resource = acquireResource();
return processData(resource)
.then(result => {
console.log('Processed:', result);
return result;
})
.catch(error => {
console.error('Error:', error);
throw error;
})
.finally(() => {
releaseResource(resource);
console.log('Resource released');
});
}
// Finally doesn't affect the chain value
Promise.resolve('success')
.then(value => {
console.log('First then:', value);
return value;
})
.finally(() => {
console.log('Finally runs');
return 'ignored'; // This value is ignored
})
.then(value => {
console.log('Second then:', value); // Still "success"
});
Promise Utilities
Built-in Promise utility functions for common operations.
Promise.resolve and Promise.reject
// Promise.resolve - create a resolved promise
const resolvedPromise = Promise.resolve('Success');
resolvedPromise.then(value => console.log(value)); // "Success"
// Promise.resolve with thenables
const thenable = {
then: (resolve) => resolve('From thenable')
};
Promise.resolve(thenable)
.then(value => console.log(value)); // "From thenable"
// Promise.reject - create a rejected promise
const rejectedPromise = Promise.reject('Error');
rejectedPromise.catch(error => console.error(error)); // "Error"
// Utility functions
function success(value) {
return Promise.resolve(value);
}
function failure(reason) {
return Promise.reject(reason);
}
Promise.try
// Promise.try (proposal, can be polyfilled)
function promiseTry(fn) {
return new Promise((resolve, reject) => {
try {
resolve(fn());
} catch (error) {
reject(error);
}
});
}
// Usage
promiseTry(() => {
const result = riskyOperation();
return result;
})
.then(result => console.log(result))
.catch(error => console.error(error));
// Modern alternative using async function
const asyncTry = async (fn) => {
try {
return await fn();
} catch (error) {
throw error;
}
};
Promise Utilities
// Delay utility
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Usage
delay(1000).then(() => console.log('1 second later'));
// Timeout utility
function timeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
)
]);
}
// Retry utility
function retry(fn, times = 3, delay = 1000) {
return new Promise((resolve, reject) => {
const attempt = (remaining) => {
fn()
.then(resolve)
.catch(error => {
if (remaining <= 1) {
reject(error);
} else {
setTimeout(() => attempt(remaining - 1), delay);
}
});
};
attempt(times);
});
}
// Usage
retry(() => fetch('/api/data'), 3, 1000)
.then(data => console.log(data))
.catch(error => console.error('Failed after retries:', error));
Promise Patterns
Common patterns and techniques for working with Promises.
Sequential Processing
// Process items sequentially
function processSequentially(items) {
return items.reduce((promise, item) => {
return promise.then(results => {
return processItem(item).then(result => {
return results.concat(result);
});
});
}, Promise.resolve([]));
}
// Alternative sequential processing
async function processSequentiallyAsync(items) {
const results = [];
for (const item of items) {
const result = await processItem(item);
results.push(result);
}
return results;
}
// Usage
const items = [1, 2, 3, 4, 5];
processSequentially(items)
.then(results => console.log('Sequential results:', results));
Parallel Processing
// Process items in parallel
function processInParallel(items) {
const promises = items.map(item => processItem(item));
return Promise.all(promises);
}
// Process with concurrency limit
function processWithConcurrency(items, concurrency = 3) {
return new Promise((resolve, reject) => {
const results = [];
let completed = 0;
let started = 0;
function start() {
if (started >= items.length) return;
const index = started++;
const item = items[index];
processItem(item)
.then(result => {
results[index] = result;
completed++;
if (completed === items.length) {
resolve(results);
} else {
start();
}
})
.catch(reject);
if (started < Math.min(items.length, concurrency)) {
start();
}
}
start();
});
}
Caching with Promises
// Promise-based cache
class PromiseCache {
constructor() {
this.cache = new Map();
this.pending = new Map();
}
get(key, fetchFn) {
// Return cached promise if available
if (this.cache.has(key)) {
return this.cache.get(key);
}
// Return pending promise if already fetching
if (this.pending.has(key)) {
return this.pending.get(key);
}
// Start new fetch
const promise = fetchFn(key);
this.pending.set(key, promise);
promise
.then(result => {
this.cache.set(key, result);
this.pending.delete(key);
})
.catch(() => {
this.pending.delete(key);
});
return promise;
}
}
// Usage
const cache = new PromiseCache();
function getUserData(userId) {
return cache.get(userId, id => fetch(`/api/users/${id}`).then(r => r.json()));
}
// Multiple calls for same ID
getUserData(1); // Starts fetch
getUserData(1); // Returns same promise
getUserData(1); // Returns same promise
Promise and Callbacks
Converting callback-based code to Promises and vice versa.
Callback to Promise
// Convert callback function to Promise
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
}
// Usage
const readFile = promisify(require('fs').readFile);
readFile('file.txt', 'utf8')
.then(content => console.log(content))
.catch(error => console.error(error));
// Node.js util.promisify
const { promisify } = require('util');
const readFileAsync = promisify(require('fs').readFile);
// Event-based to Promise
function once(emitter, event) {
return new Promise((resolve, reject) => {
emitter.once(event, resolve);
emitter.once('error', reject);
});
}
// Usage
once(emitter, 'data')
.then(data => console.log('Got data:', data));
Promise to Callback
// Convert Promise to callback
function callbackify(promiseFn) {
return function(...args) {
const callback = args.pop();
promiseFn(...args)
.then(result => callback(null, result))
.catch(error => callback(error));
};
}
// Usage
const fetchUserCallback = callbackify(fetchUser);
fetchUserCallback(1, (error, user) => {
if (error) {
console.error('Error:', error);
} else {
console.log('User:', user);
}
});
Debugging Promises
Techniques for debugging Promise-based code.
Unhandled Rejections
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Prevent process from exiting
});
process.on('rejectionHandled', (promise, reason) => {
console.log('Rejection was handled:', reason);
});
// Browser equivalent
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled promise rejection:', event.reason);
event.preventDefault(); // Prevent default browser behavior
});
window.addEventListener('rejectionhandled', event => {
console.log('Promise rejection was handled:', event.reason);
});
Promise Inspection
// Inspect promise state
function inspectPromise(promise) {
const result = { state: 'pending' };
promise
.then(value => {
result.state = 'fulfilled';
result.value = value;
})
.catch(error => {
result.state = 'rejected';
result.reason = error;
});
return result;
}
// Debug utility
function debug(promise, name = 'Promise') {
console.time(name);
return promise
.then(result => {
console.timeEnd(name);
console.log(`${name} fulfilled:`, result);
return result;
})
.catch(error => {
console.timeEnd(name);
console.error(`${name} rejected:`, error);
throw error;
});
}
// Usage
debug(fetchData('/api/data'), 'API Call');
Performance Considerations
Optimizing Promise-based code for better performance.
Microtasks vs Macrotasks
// Understanding microtask queue
console.log('Start');
setTimeout(() => console.log('Macrotask 1'), 0);
setTimeout(() => console.log('Macrotask 2'), 0);
Promise.resolve().then(() => console.log('Microtask 1'));
Promise.resolve().then(() => console.log('Microtask 2'));
console.log('End');
// Output: Start, End, Microtask 1, Microtask 2, Macrotask 1, Macrotask 2
// Microtasks run before next macrotask
Promise.resolve().then(() => {
console.log('Microtask');
setTimeout(() => console.log('Macrotask'), 0);
});
setTimeout(() => console.log('Macrotask'), 0);
Memory Management
// Avoid memory leaks with promises
function createLeakyPromise() {
const promise = new Promise(resolve => {
setTimeout(() => resolve('done'), 1000);
});
// Don't hold references to resolved promises
// promise = null; // Allow garbage collection
return promise;
}
// Process large datasets in chunks
function processLargeDataset(data, chunkSize = 1000) {
let index = 0;
function processChunk() {
if (index >= data.length) {
return Promise.resolve();
}
const chunk = data.slice(index, index + chunkSize);
index += chunkSize;
return processChunkData(chunk).then(() => {
// Allow event loop to process other tasks
return new Promise(resolve => setTimeout(resolve, 0));
}).then(processChunk);
}
return processChunk();
}
Best Practices
Guidelines for writing clean and maintainable Promise-based code.
Error Handling
// Always handle rejections
fetchData('/api/data')
.then(data => console.log(data))
.catch(error => console.error(error)); // Always include catch
// Handle errors at appropriate level
function fetchUserData(userId) {
return fetchUser(userId)
.catch(error => {
console.error('Failed to fetch user:', error);
throw error; // Re-throw for caller to handle
});
}
// Create error handling utilities
const withErrorHandling = (promise, errorHandler) => {
return promise.catch(errorHandler);
};
// Usage
withErrorHandling(
fetchData('/api/data'),
error => console.error('Fetch failed:', error)
);
Promise Composition
// Compose promises for better reusability
const fetchUser = id => fetch(`/api/users/${id}`).then(r => r.json());
const fetchUserPosts = user => fetch(`/api/users/${user.id}/posts`).then(r => r.json());
// Compose functions
const fetchUserWithPosts = id =>
fetchUser(id)
.then(user =>
fetchUserPosts(user)
.then(posts => ({ user, posts }))
);
// Pipeline pattern
const pipe = (...fns) => (value) =>
fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(value));
// Usage
const process = pipe(
fetchUser,
validateUser,
enrichUserData,
saveUser
);
process(userId).then(result => console.log(result));
Code Organization
// Separate async logic from business logic
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
function processUserData(userData) {
return {
fullName: `${userData.firstName} ${userData.lastName}`,
displayName: userData.firstName,
age: calculateAge(userData.birthDate)
};
}
// Service layer
class UserService {
async getUserProfile(userId) {
const userData = await fetchUserData(userId);
return processUserData(userData);
}
async updateUserProfile(userId, data) {
const processedData = processUserData(data);
return updateUser(userId, processedData);
}
}
Summary
Key Takeaways
- Promises represent eventual completion of async operations
- Three states: pending, fulfilled, rejected
- Use Promise.all for parallel execution
- Always handle promise rejections
- Promise methods provide powerful composition tools
Best Practices
- Always include error handling with catch()
- Use Promise.all for parallel operations
- Separate async logic from business logic
- Handle unhandled rejections globally
- Use appropriate Promise methods for different scenarios
Common Pitfalls
- Forgetting to handle promise rejections
- Creating unnecessary promise chains
- Mixing callbacks and promises incorrectly
- Not understanding microtask vs macrotask
- Creating memory leaks with unresolved promises