Learning Objectives

  • Create reusable Promise utilities
  • Master the promisify pattern
  • Build delay/sleep functions
  • Develop a Promise utility library

Why Build Promise Utilities?

Reusable utilities make your code:

Essential Promise Utilities

1. Delay/Sleep Function

Create a Promise-based delay:

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// Usage
await delay(1000); // Wait 1 second
console.log('1 second later');

With value:

function delay(ms, value) {
  return new Promise(resolve => {
    setTimeout(() => resolve(value), ms);
  });
}

const result = await delay(1000, 'Hello');
console.log(result); // 'Hello' after 1 second

2. Timeout Wrapper

Add timeout to any Promise:

function timeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) => {
      setTimeout(() => reject(new Error('Timeout')), ms);
    })
  ]);
}

// Usage
try {
  const data = await timeout(fetchData(), 5000);
  console.log(data);
} catch (error) {
  console.error('Request timed out');
}

3. Retry Function

Retry failed operations:

async function retry(fn, retries = 3, delay = 1000) {
  try {
    return await fn();
  } catch (error) {
    if (retries <= 0) {
      throw error;
    }
    
    console.log(`Retrying... (${retries} attempts left)`);
    await new Promise(resolve => setTimeout(resolve, delay));
    return retry(fn, retries - 1, delay);
  }
}

// Usage
const data = await retry(() => fetchData(), 3, 1000);

4. Promisify Pattern

Convert callback-based functions to Promises:

function promisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      });
    });
  };
}

// Usage
const fs = require('fs');
const readFileAsync = promisify(fs.readFile);

const content = await readFileAsync('file.txt', 'utf8');

5. Sequential Execution

Run Promises one after another:

async function sequential(tasks) {
  const results = [];
  
  for (const task of tasks) {
    const result = await task();
    results.push(result);
  }
  
  return results;
}

// Usage
const results = await sequential([
  () => fetchUser(1),
  () => fetchUser(2),
  () => fetchUser(3)
]);

6. Parallel with Limit

Control concurrency:

async function parallelLimit(tasks, limit) {
  const results = [];
  const executing = [];
  
  for (const task of tasks) {
    const promise = Promise.resolve().then(() => task());
    results.push(promise);
    
    if (limit <= tasks.length) {
      const e = promise.then(() => {
        executing.splice(executing.indexOf(e), 1);
      });
      executing.push(e);
      
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }
  
  return Promise.all(results);
}

// Usage: Run max 3 tasks at once
const results = await parallelLimit(tasks, 3);

7. Map with Promises

Transform array items asynchronously:

async function mapAsync(array, asyncFn) {
  return Promise.all(array.map(asyncFn));
}

// Usage
const userIds = [1, 2, 3, 4, 5];
const users = await mapAsync(userIds, id => fetchUser(id));

8. Filter with Promises

Filter array with async predicate:

async function filterAsync(array, asyncPredicate) {
  const results = await Promise.all(
    array.map(async item => ({
      item,
      pass: await asyncPredicate(item)
    }))
  );
  
  return results
    .filter(result => result.pass)
    .map(result => result.item);
}

// Usage
const activeUsers = await filterAsync(users, 
  user => checkIfActive(user.id)
);

Building a Complete Utility Library

// promiseUtils.js
const PromiseUtils = {
  delay: (ms, value) => new Promise(resolve => 
    setTimeout(() => resolve(value), ms)
  ),
  
  timeout: (promise, ms) => Promise.race([
    promise,
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('Timeout')), ms)
    )
  ]),
  
  retry: async (fn, retries = 3, delay = 1000) => {
    try {
      return await fn();
    } catch (error) {
      if (retries <= 0) throw error;
      await PromiseUtils.delay(delay);
      return PromiseUtils.retry(fn, retries - 1, delay * 2);
    }
  },
  
  sequential: async (tasks) => {
    const results = [];
    for (const task of tasks) {
      results.push(await task());
    }
    return results;
  },
  
  mapAsync: (array, fn) => Promise.all(array.map(fn)),
  
  filterAsync: async (array, predicate) => {
    const results = await Promise.all(
      array.map(async item => ({
        item,
        pass: await predicate(item)
      }))
    );
    return results.filter(r => r.pass).map(r => r.item);
  }
};

export default PromiseUtils;

Real-World Example: API Client with Utilities

class APIClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }
  
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    
    // Add timeout
    const fetchWithTimeout = PromiseUtils.timeout(
      fetch(url, options),
      5000
    );
    
    // Add retry logic
    return PromiseUtils.retry(
      async () => {
        const response = await fetchWithTimeout;
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }
        return response.json();
      },
      3,
      1000
    );
  }
  
  async batchGet(endpoints) {
    return PromiseUtils.mapAsync(
      endpoints,
      endpoint => this.request(endpoint)
    );
  }
}

// Usage
const api = new APIClient('https://api.example.com');
const data = await api.batchGet(['/users', '/posts', '/comments']);

Key Takeaways

  • ✅ Build reusable utilities for common async patterns
  • delay() creates Promise-based timeouts
  • timeout() adds time limits to Promises
  • retry() implements automatic retry logic
  • promisify() converts callbacks to Promises
  • ✅ Utility libraries improve code maintainability

Next Steps

Continue exploring advanced patterns or jump to testing async code in Section 7!