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.