Back to Blog
JavaScriptPromisesAsyncPatterns

JavaScript Promise Patterns

Master JavaScript Promise patterns. From basics to advanced composition to error handling.

B
Bootspring Team
Engineering
November 9, 2020
8 min read

Promises are fundamental to async JavaScript. Here are essential patterns.

Promise Basics#

1// Creating promises 2const promise = new Promise((resolve, reject) => { 3 const success = doSomething(); 4 if (success) { 5 resolve(success); 6 } else { 7 reject(new Error('Operation failed')); 8 } 9}); 10 11// Consuming promises 12promise 13 .then(result => console.log(result)) 14 .catch(error => console.error(error)) 15 .finally(() => console.log('Done')); 16 17// Promise.resolve and Promise.reject 18const resolved = Promise.resolve(42); 19const rejected = Promise.reject(new Error('Failed')); 20 21// Async value wrapping 22function maybeAsync(value) { 23 return Promise.resolve(value); 24}

Chaining Patterns#

1// Sequential operations 2fetchUser(userId) 3 .then(user => fetchOrders(user.id)) 4 .then(orders => processOrders(orders)) 5 .then(results => saveResults(results)) 6 .catch(error => handleError(error)); 7 8// Transform values 9fetch('/api/data') 10 .then(response => response.json()) 11 .then(data => data.items) 12 .then(items => items.filter(item => item.active)) 13 .then(activeItems => activeItems.map(item => item.name)); 14 15// Returning values vs promises 16Promise.resolve(5) 17 .then(x => x * 2) // Returns 10 18 .then(x => Promise.resolve(x + 3)) // Returns Promise<13> 19 .then(x => console.log(x)); // Logs: 13 20 21// Skipping steps conditionally 22getUser(id) 23 .then(user => { 24 if (user.isAdmin) { 25 return getAdminData(user); 26 } 27 return user; // Skip admin data fetch 28 }) 29 .then(data => render(data));

Parallel Execution#

1// Promise.all - all must succeed 2const promises = [ 3 fetch('/api/users'), 4 fetch('/api/orders'), 5 fetch('/api/products'), 6]; 7 8Promise.all(promises) 9 .then(([users, orders, products]) => { 10 // All succeeded 11 }) 12 .catch(error => { 13 // First failure 14 }); 15 16// With destructuring and transformation 17Promise.all([ 18 fetch('/api/users').then(r => r.json()), 19 fetch('/api/orders').then(r => r.json()), 20]) 21 .then(([users, orders]) => { 22 return { users, orders }; 23 }); 24 25// Dynamic parallel requests 26const userIds = [1, 2, 3, 4, 5]; 27const userPromises = userIds.map(id => fetchUser(id)); 28const users = await Promise.all(userPromises);

Handling Partial Failures#

1// Promise.allSettled - always resolves 2const results = await Promise.allSettled([ 3 fetch('/api/critical'), 4 fetch('/api/optional'), 5 fetch('/api/another'), 6]); 7 8const successful = results 9 .filter(r => r.status === 'fulfilled') 10 .map(r => r.value); 11 12const failed = results 13 .filter(r => r.status === 'rejected') 14 .map(r => r.reason); 15 16// Process settled results 17results.forEach((result, index) => { 18 if (result.status === 'fulfilled') { 19 console.log(`Request ${index} succeeded:`, result.value); 20 } else { 21 console.log(`Request ${index} failed:`, result.reason); 22 } 23}); 24 25// With fallbacks 26async function fetchWithFallback(urls) { 27 const results = await Promise.allSettled( 28 urls.map(url => fetch(url).then(r => r.json())) 29 ); 30 31 return results.map((result, index) => ({ 32 url: urls[index], 33 data: result.status === 'fulfilled' ? result.value : null, 34 error: result.status === 'rejected' ? result.reason : null, 35 })); 36}

Racing Promises#

1// Promise.race - first to settle 2const fastest = await Promise.race([ 3 fetch('/api/server1/data'), 4 fetch('/api/server2/data'), 5]); 6 7// Timeout pattern 8function withTimeout(promise, ms) { 9 const timeout = new Promise((_, reject) => { 10 setTimeout(() => reject(new Error('Timeout')), ms); 11 }); 12 return Promise.race([promise, timeout]); 13} 14 15const result = await withTimeout(fetch('/api/slow'), 5000); 16 17// Promise.any - first to succeed (ES2021) 18try { 19 const first = await Promise.any([ 20 fetch('/api/server1'), 21 fetch('/api/server2'), 22 fetch('/api/server3'), 23 ]); 24 // First successful response 25} catch (error) { 26 // All failed - AggregateError 27 console.log(error.errors); 28}

Error Handling Patterns#

1// Catch and recover 2fetchData() 3 .catch(error => { 4 console.warn('Primary fetch failed, using cache'); 5 return getCachedData(); 6 }) 7 .then(data => process(data)); 8 9// Catch and rethrow with context 10fetchUser(id) 11 .catch(error => { 12 throw new Error(`Failed to fetch user ${id}: ${error.message}`); 13 }); 14 15// Error boundaries in chains 16Promise.resolve() 17 .then(() => riskyOperation1()) 18 .catch(error => { 19 console.error('Step 1 failed:', error); 20 return fallback1(); 21 }) 22 .then(() => riskyOperation2()) 23 .catch(error => { 24 console.error('Step 2 failed:', error); 25 return fallback2(); 26 }); 27 28// Typed error handling 29class NetworkError extends Error {} 30class ValidationError extends Error {} 31 32fetchData() 33 .catch(error => { 34 if (error instanceof NetworkError) { 35 return retryFetch(); 36 } 37 if (error instanceof ValidationError) { 38 return getDefaultData(); 39 } 40 throw error; // Rethrow unknown errors 41 });

Retry Patterns#

1// Simple retry 2async function retry(fn, attempts = 3) { 3 for (let i = 0; i < attempts; i++) { 4 try { 5 return await fn(); 6 } catch (error) { 7 if (i === attempts - 1) throw error; 8 } 9 } 10} 11 12// Retry with delay 13async function retryWithDelay(fn, attempts = 3, delay = 1000) { 14 for (let i = 0; i < attempts; i++) { 15 try { 16 return await fn(); 17 } catch (error) { 18 if (i === attempts - 1) throw error; 19 await new Promise(r => setTimeout(r, delay)); 20 } 21 } 22} 23 24// Exponential backoff 25async function retryExponential(fn, options = {}) { 26 const { 27 attempts = 3, 28 initialDelay = 1000, 29 maxDelay = 30000, 30 factor = 2, 31 } = options; 32 33 let delay = initialDelay; 34 35 for (let i = 0; i < attempts; i++) { 36 try { 37 return await fn(); 38 } catch (error) { 39 if (i === attempts - 1) throw error; 40 41 await new Promise(r => setTimeout(r, delay)); 42 delay = Math.min(delay * factor, maxDelay); 43 } 44 } 45} 46 47// Retry with condition 48async function retryWhen(fn, shouldRetry, maxAttempts = 3) { 49 for (let i = 0; i < maxAttempts; i++) { 50 try { 51 return await fn(); 52 } catch (error) { 53 if (!shouldRetry(error) || i === maxAttempts - 1) { 54 throw error; 55 } 56 } 57 } 58} 59 60// Usage 61await retryWhen( 62 () => fetch('/api/data'), 63 error => error.status === 503, // Retry only on 503 64 5 65);

Debouncing and Throttling#

1// Debounced promise 2function debouncePromise(fn, delay) { 3 let timeoutId; 4 let pendingPromise; 5 let resolve; 6 let reject; 7 8 return function (...args) { 9 clearTimeout(timeoutId); 10 11 if (!pendingPromise) { 12 pendingPromise = new Promise((res, rej) => { 13 resolve = res; 14 reject = rej; 15 }); 16 } 17 18 timeoutId = setTimeout(async () => { 19 try { 20 const result = await fn.apply(this, args); 21 resolve(result); 22 } catch (error) { 23 reject(error); 24 } finally { 25 pendingPromise = null; 26 } 27 }, delay); 28 29 return pendingPromise; 30 }; 31} 32 33// Throttled promise 34function throttlePromise(fn, delay) { 35 let lastCall = 0; 36 let pendingPromise = null; 37 38 return async function (...args) { 39 const now = Date.now(); 40 41 if (now - lastCall >= delay) { 42 lastCall = now; 43 return fn.apply(this, args); 44 } 45 46 if (!pendingPromise) { 47 pendingPromise = new Promise(resolve => { 48 setTimeout(() => { 49 lastCall = Date.now(); 50 pendingPromise = null; 51 resolve(fn.apply(this, args)); 52 }, delay - (now - lastCall)); 53 }); 54 } 55 56 return pendingPromise; 57 }; 58}

Queue Patterns#

1// Sequential queue 2class PromiseQueue { 3 constructor() { 4 this.queue = Promise.resolve(); 5 } 6 7 add(fn) { 8 this.queue = this.queue.then(fn).catch(() => {}); 9 return this.queue; 10 } 11} 12 13const queue = new PromiseQueue(); 14queue.add(() => fetch('/api/1')); 15queue.add(() => fetch('/api/2')); 16 17// Concurrent queue with limit 18class ConcurrentQueue { 19 constructor(concurrency = 3) { 20 this.concurrency = concurrency; 21 this.running = 0; 22 this.queue = []; 23 } 24 25 add(fn) { 26 return new Promise((resolve, reject) => { 27 this.queue.push({ fn, resolve, reject }); 28 this.process(); 29 }); 30 } 31 32 process() { 33 while (this.running < this.concurrency && this.queue.length > 0) { 34 const { fn, resolve, reject } = this.queue.shift(); 35 this.running++; 36 37 fn() 38 .then(resolve) 39 .catch(reject) 40 .finally(() => { 41 this.running--; 42 this.process(); 43 }); 44 } 45 } 46} 47 48// Usage 49const limiter = new ConcurrentQueue(3); 50const results = await Promise.all( 51 urls.map(url => limiter.add(() => fetch(url))) 52);

Caching Patterns#

1// Simple memoization 2function memoizePromise(fn) { 3 const cache = new Map(); 4 5 return async function (key) { 6 if (cache.has(key)) { 7 return cache.get(key); 8 } 9 10 const promise = fn(key); 11 cache.set(key, promise); 12 13 try { 14 return await promise; 15 } catch (error) { 16 cache.delete(key); // Remove failed promises 17 throw error; 18 } 19 }; 20} 21 22// Cache with expiration 23function cachePromise(fn, ttl = 60000) { 24 const cache = new Map(); 25 26 return async function (key) { 27 const cached = cache.get(key); 28 29 if (cached && Date.now() < cached.expires) { 30 return cached.value; 31 } 32 33 const value = await fn(key); 34 cache.set(key, { 35 value, 36 expires: Date.now() + ttl, 37 }); 38 39 return value; 40 }; 41} 42 43// Stale-while-revalidate 44function swr(fn, ttl = 60000) { 45 const cache = new Map(); 46 47 return async function (key) { 48 const cached = cache.get(key); 49 50 if (cached) { 51 if (Date.now() > cached.expires) { 52 // Revalidate in background 53 fn(key).then(value => { 54 cache.set(key, { value, expires: Date.now() + ttl }); 55 }); 56 } 57 return cached.value; 58 } 59 60 const value = await fn(key); 61 cache.set(key, { value, expires: Date.now() + ttl }); 62 return value; 63 }; 64}

Cancellation Patterns#

1// Using AbortController 2async function fetchWithCancel(url) { 3 const controller = new AbortController(); 4 5 const promise = fetch(url, { signal: controller.signal }) 6 .then(r => r.json()); 7 8 promise.cancel = () => controller.abort(); 9 return promise; 10} 11 12// Cancellable promise wrapper 13function cancellable(promise) { 14 let cancelled = false; 15 16 const wrapped = new Promise((resolve, reject) => { 17 promise 18 .then(value => { 19 if (!cancelled) resolve(value); 20 }) 21 .catch(error => { 22 if (!cancelled) reject(error); 23 }); 24 }); 25 26 wrapped.cancel = () => { cancelled = true; }; 27 return wrapped; 28}

Best Practices#

Error Handling: ✓ Always handle rejections ✓ Use specific error types ✓ Add context when rethrowing ✓ Consider partial failures Performance: ✓ Use Promise.all for parallel work ✓ Implement timeouts ✓ Consider caching ✓ Limit concurrency Code Quality: ✓ Prefer async/await for readability ✓ Keep chains short ✓ Extract complex logic to functions ✓ Document expected errors Patterns: ✓ Use retry for transient failures ✓ Implement circuit breakers ✓ Consider queue patterns ✓ Use cancellation when needed

Conclusion#

Promise patterns enable robust async code. Use parallel execution for performance, proper error handling for reliability, and patterns like retry and caching for resilience. Master these patterns to write production-ready async JavaScript.

Share this article

Help spread the word about Bootspring