Skip to main content
โšก Calmops

Advanced Promise Patterns in JavaScript

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

  1. Use Promise.all() for independent operations:

    // โœ… Good
    const [a, b, c] = await Promise.all([op1(), op2(), op3()]);
    
  2. Use AbortController for cancellation:

    // โœ… Good
    const controller = new AbortController();
    fetch(url, { signal: controller.signal });
    
  3. Implement retry with backoff:

    // โœ… Good
    await retryWithBackoff(fn, 3, 1000);
    
  4. Add timeouts to external operations:

    // โœ… Good
    await withTimeout(fetch(url), 5000);
    

Common Mistakes

  1. Not handling promise rejection:

    // โŒ Bad
    Promise.all(promises);
    
    // โœ… Good
    Promise.all(promises).catch(error => {
      console.error('Error:', error);
    });
    
  2. Ignoring cancellation:

    // โŒ Bad - no way to cancel
    fetch(url);
    
    // โœ… Good - can cancel
    fetch(url, { signal: controller.signal });
    
  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

Comments