Advanced Promise Patterns in JavaScript
Advanced promise patterns enable sophisticated async workflows. This article covers composition, cancellation, pooling, and practical patterns.
Introduction
Advanced promise patterns:
- Compose complex workflows
- Cancel long-running operations
- Manage promise pools
- Handle sophisticated scenarios
- Build robust async systems
Understanding advanced patterns helps you:
- Build complex async workflows
- Handle cancellation
- Manage resources efficiently
- Create reusable patterns
Promise Composition
Sequential Composition
// โ
Good: Sequential promise composition
function sequence(...fns) {
return fns.reduce(
(promise, fn) => promise.then(fn),
Promise.resolve()
);
}
// Usage
sequence(
() => fetch('/api/user').then(r => r.json()),
user => fetch(`/api/posts/${user.id}`).then(r => r.json()),
posts => fetch(`/api/comments/${posts[0].id}`).then(r => r.json())
).then(comments => {
console.log('Comments:', comments);
});
Parallel Composition
// โ
Good: Parallel promise composition
function parallel(...fns) {
return Promise.all(fns.map(fn => fn()));
}
// Usage
parallel(
() => fetch('/api/users').then(r => r.json()),
() => fetch('/api/posts').then(r => r.json()),
() => fetch('/api/comments').then(r => r.json())
).then(([users, posts, comments]) => {
console.log('All data:', { users, posts, comments });
});
Conditional Composition
// โ
Good: Conditional promise composition
function conditional(condition, trueFn, falseFn) {
return condition ? trueFn() : falseFn();
}
// Usage
conditional(
user.isAdmin,
() => fetch('/api/admin/data').then(r => r.json()),
() => fetch('/api/user/data').then(r => r.json())
).then(data => {
console.log('Data:', data);
});
Promise Cancellation
Cancellation Token Pattern
// โ
Good: Cancellation token
class CancellationToken {
constructor() {
this.cancelled = false;
this.callbacks = [];
}
cancel() {
this.cancelled = true;
this.callbacks.forEach(cb => cb());
}
onCancel(callback) {
this.callbacks.push(callback);
}
throwIfCancelled() {
if (this.cancelled) {
throw new Error('Operation cancelled');
}
}
}
// Usage
const token = new CancellationToken();
async function fetchWithCancellation(url) {
try {
token.throwIfCancelled();
const response = await fetch(url);
token.throwIfCancelled();
return response.json();
} catch (error) {
if (error.message === 'Operation cancelled') {
console.log('Fetch cancelled');
}
throw error;
}
}
// Cancel after 5 seconds
setTimeout(() => token.cancel(), 5000);
AbortController Pattern
// โ
Good: Use AbortController for cancellation
const controller = new AbortController();
async function fetchWithAbort(url) {
try {
const response = await fetch(url, {
signal: controller.signal
});
return response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request aborted');
}
throw error;
}
}
// Abort after 5 seconds
setTimeout(() => controller.abort(), 5000);
// Usage
fetchWithAbort('/api/data');
Cancellable Promise Wrapper
// โ
Good: Wrap promise with cancellation
class CancellablePromise {
constructor(fn) {
this.cancelled = false;
this.promise = new Promise((resolve, reject) => {
fn(
value => !this.cancelled && resolve(value),
error => !this.cancelled && reject(error)
);
});
}
cancel() {
this.cancelled = true;
}
then(onFulfilled, onRejected) {
return this.promise.then(onFulfilled, onRejected);
}
catch(onRejected) {
return this.promise.catch(onRejected);
}
}
// Usage
const cancellable = new CancellablePromise((resolve, reject) => {
const timeout = setTimeout(() => resolve('Done'), 5000);
this.cancel = () => {
clearTimeout(timeout);
reject(new Error('Cancelled'));
};
});
// Cancel after 2 seconds
setTimeout(() => cancellable.cancel(), 2000);
Promise Pooling
Promise Pool
// โ
Good: Promise pool for concurrent operations
class PromisePool {
constructor(concurrency) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
async run(fn) {
while (this.running >= this.concurrency) {
await new Promise(resolve => this.queue.push(resolve));
}
this.running++;
try {
return await fn();
} finally {
this.running--;
const resolve = this.queue.shift();
if (resolve) resolve();
}
}
async runAll(fns) {
return Promise.all(fns.map(fn => this.run(fn)));
}
}
// Usage
const pool = new PromisePool(3);
const tasks = Array.from({ length: 10 }, (_, i) => () =>
new Promise(resolve => {
console.log(`Task ${i} started`);
setTimeout(() => {
console.log(`Task ${i} completed`);
resolve(i);
}, 1000);
})
);
const results = await pool.runAll(tasks);
console.log('Results:', results);
Advanced Patterns
Retry with Exponential Backoff
// โ
Good: Retry with exponential backoff
async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries - 1) throw error;
const delay = baseDelay * Math.pow(2, attempt);
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const data = await retryWithBackoff(
() => fetch('/api/data').then(r => r.json()),
3,
1000
);
Timeout Wrapper
// โ
Good: Add timeout to any promise
function withTimeout(promise, timeoutMs) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeoutMs)
)
]);
}
// Usage
try {
const data = await withTimeout(
fetch('/api/data').then(r => r.json()),
5000
);
} catch (error) {
console.error('Request failed or timed out');
}
Retry with Timeout
// โ
Good: Combine retry and timeout
async function retryWithTimeout(fn, maxRetries = 3, timeoutMs = 5000) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await withTimeout(fn(), timeoutMs);
} catch (error) {
if (attempt === maxRetries - 1) throw error;
const delay = Math.pow(2, attempt) * 1000;
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const data = await retryWithTimeout(
() => fetch('/api/data').then(r => r.json()),
3,
5000
);
Promise Memoization
// โ
Good: Memoize promise results
function memoizePromise(fn) {
const cache = new Map();
const pending = new Map();
return async function(...args) {
const key = JSON.stringify(args);
// Return cached result
if (cache.has(key)) {
console.log('From cache:', key);
return cache.get(key);
}
// Return pending promise
if (pending.has(key)) {
console.log('Waiting for pending:', key);
return pending.get(key);
}
// Compute and cache
console.log('Computing:', key);
const promise = fn(...args);
pending.set(key, promise);
try {
const result = await promise;
cache.set(key, result);
return result;
} finally {
pending.delete(key);
}
};
}
// Usage
const memoizedFetch = memoizePromise(async (url) => {
const response = await fetch(url);
return response.json();
});
// Multiple calls with same URL only fetch once
Promise.all([
memoizedFetch('/api/data'),
memoizedFetch('/api/data'),
memoizedFetch('/api/data')
]);
Waterfall Pattern
// โ
Good: Waterfall pattern for sequential operations
async function waterfall(tasks, initialValue) {
let result = initialValue;
for (const task of tasks) {
result = await task(result);
}
return result;
}
// Usage
const result = await waterfall([
async (value) => {
console.log('Step 1:', value);
return value + 1;
},
async (value) => {
console.log('Step 2:', value);
return value * 2;
},
async (value) => {
console.log('Step 3:', value);
return value + 10;
}
], 5);
console.log('Final result:', result); // 22
Parallel with Fallback
// โ
Good: Parallel execution with fallback
async function parallelWithFallback(primary, fallback) {
try {
return await primary();
} catch (error) {
console.log('Primary failed, trying fallback');
return await fallback();
}
}
// Usage
const data = await parallelWithFallback(
() => fetch('https://primary-api.com/data').then(r => r.json()),
() => fetch('https://backup-api.com/data').then(r => r.json())
);
Practical Examples
API Request with Retry and Timeout
// โ
Good: Robust API request
async function robustFetch(url, options = {}) {
const {
maxRetries = 3,
timeoutMs = 5000,
backoffMultiplier = 2
} = options;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
} catch (error) {
clearTimeout(timeoutId);
if (attempt === maxRetries - 1) throw error;
const delay = Math.pow(backoffMultiplier, attempt) * 1000;
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
const data = await robustFetch('/api/data', {
maxRetries: 3,
timeoutMs: 5000
});
Batch Processing with Concurrency
// โ
Good: Batch processing with concurrency control
async function batchProcess(items, processor, concurrency = 3) {
const results = [];
const executing = [];
for (const item of items) {
const promise = Promise.resolve().then(() => processor(item));
results.push(promise);
if (concurrency <= items.length) {
executing.push(
promise.then(() => executing.splice(executing.indexOf(promise), 1))
);
if (executing.length >= concurrency) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
// Usage
const items = Array.from({ length: 100 }, (_, i) => i);
const results = await batchProcess(
items,
async (item) => {
const response = await fetch(`/api/item/${item}`);
return response.json();
},
5 // Process 5 at a time
);
Best Practices
-
Use Promise.all() for independent operations:
// โ Good const [a, b, c] = await Promise.all([op1(), op2(), op3()]); -
Use AbortController for cancellation:
// โ Good const controller = new AbortController(); fetch(url, { signal: controller.signal }); -
Implement retry with backoff:
// โ Good await retryWithBackoff(fn, 3, 1000); -
Add timeouts to external operations:
// โ Good await withTimeout(fetch(url), 5000);
Common Mistakes
-
Not handling promise rejection:
// โ Bad Promise.all(promises); // โ Good Promise.all(promises).catch(error => { console.error('Error:', error); }); -
Ignoring cancellation:
// โ Bad - no way to cancel fetch(url); // โ Good - can cancel fetch(url, { signal: controller.signal }); -
Not implementing retry:
// โ Bad - fails on first error await fetch(url); // โ Good - retries on failure await retryWithBackoff(() => fetch(url));
Summary
Advanced promise patterns enable sophisticated async workflows. Key takeaways:
- Compose promises for complex workflows
- Implement cancellation with AbortController
- Use promise pools for concurrency control
- Combine retry, timeout, and backoff
- Memoize promise results
- Handle errors properly
- Build robust async systems
Related Resources
Next Steps
- Learn about Concurrency Patterns in JavaScript
- Explore Stream Processing Basics
- Study Generators and Iterators
- Practice with complex async workflows
- Build production-ready async systems
Comments