Skip to main content

Advanced Promise Patterns in JavaScript

Created: May 8, 2026 Larry Qu 7 min read

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

  1. Use Promise.all() for independent operations:
    // ✅ Good
    const [a, b, c] = await Promise.all([op1(), op2(), op3()]);
    ```javascript
    
  2. Use AbortController for cancellation:
    // ✅ Good
    const controller = new AbortController();
    fetch(url, { signal: controller.signal });
    ```javascript
    
  3. Implement retry with backoff:
    // ✅ Good
    await retryWithBackoff(fn, 3, 1000);
    ```javascript
    
  4. Add timeouts to external operations:
    // ✅ Good
    await withTimeout(fetch(url), 5000);
    ```javascript
    

Common Mistakes

  1. Not handling promise rejection:
    // ❌ Bad
    Promise.all(promises);
    
    // ✅ Good
    Promise.all(promises).catch(error => {
      console.error('Error:', error);
    });
    ```javascript
    
  2. Ignoring cancellation:
    // ❌ Bad - no way to cancel
    fetch(url);
    
    // ✅ Good - can cancel
    fetch(url, { signal: controller.signal });
    ```javascript
    
  3. 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

Next Steps

Resources

Comments

Share this article

Scan to read on mobile