Promise Static Methods

Mastering the Promise API's powerful collective operations

Introduction to Promise Static Methods

While we've explored creating individual Promises and chaining them, the Promise API also provides powerful static methods that operate on collections of promises or simplify common promise patterns. These methods allow us to coordinate multiple asynchronous operations in sophisticated ways.

Static methods are called directly on the Promise constructor itself, not on Promise instances. They help us coordinate multiple promises, transform values into promises, or create pre-resolved promises.

graph TD A["Promise Class"] --> B["Promise.all()"] A --> C["Promise.race()"] A --> D["Promise.allSettled()"] A --> E["Promise.any()"] A --> F["Promise.resolve()"] A --> G["Promise.reject()"] style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#bbf,stroke:#333,stroke-width:1px style C fill:#bbf,stroke:#333,stroke-width:1px style D fill:#bbf,stroke:#333,stroke-width:1px style E fill:#bbf,stroke:#333,stroke-width:1px style F fill:#ddf,stroke:#333,stroke-width:1px style G fill:#ddf,stroke:#333,stroke-width:1px

Creating Pre-Resolved Promises

Promise.resolve()

Promise.resolve() creates a Promise that is already resolved with a given value. This is useful when you need to convert a synchronous value into a Promise, ensuring consistency in your API.

Real-World Analogy

Think of Promise.resolve() as sending a pre-signed contract. Instead of waiting for negotiations and signatures, you're providing something that's already finalized and ready to use.

// Creating a resolved promise
const resolvedPromise = Promise.resolve('Data is ready');

// Using the pre-resolved promise
resolvedPromise.then(data => {
  console.log(data); // Outputs: "Data is ready"
});

// Practical example: Creating an API that always returns promises
function fetchUserData(userId) {
  const cachedData = checkCache(userId);
  
  if (cachedData) {
    // Return a pre-resolved promise with cached data
    return Promise.resolve(cachedData);
  }
  
  // Otherwise, make the actual API call which returns a promise
  return makeAPICall(userId);
}

Promise.reject()

Similarly, Promise.reject() creates a Promise that is already rejected with a given reason. This is useful for error handling and creating predictable failure paths.

// Creating a rejected promise
const rejectedPromise = Promise.reject(new Error('Access denied'));

// Handling the rejection
rejectedPromise
  .catch(error => {
    console.error(error.message); // Outputs: "Access denied"
  });

// Practical example: Function that validates input before proceeding
function processUserInput(input) {
  if (!input) {
    // Return early with a rejected promise
    return Promise.reject(new Error('Input cannot be empty'));
  }
  
  // Process the valid input asynchronously
  return asyncProcessing(input);
}

Coordinating Multiple Promises

Promise.all()

Promise.all() takes an iterable of promises and returns a new promise that resolves when all input promises have resolved, or rejects if any of the promises reject.

The resolved value is an array of all the fulfillment values, in the same order as the promises provided.

Real-World Analogy

Promise.all() is like coordinating a dinner party where you need all ingredients to arrive before you can start cooking. If even one ingredient doesn't arrive (rejects), dinner is canceled.

Promise 1 Promise 2 Promise 3 Promise.all() Result Array Resolves when ALL succeed Rejects if ANY fails
// Example: Loading multiple resources in parallel
const fetchUserProfile = fetch('/api/user').then(res => res.json());
const fetchUserPosts = fetch('/api/posts').then(res => res.json());
const fetchUserAnalytics = fetch('/api/analytics').then(res => res.json());

// Wait for all data to be available before rendering the dashboard
Promise.all([fetchUserProfile, fetchUserPosts, fetchUserAnalytics])
  .then(([profile, posts, analytics]) => {
    renderDashboard(profile, posts, analytics);
  })
  .catch(error => {
    showErrorMessage('Failed to load dashboard data');
    console.error(error);
  });

// Real-world example: E-commerce checkout process
function processCheckout(cartId) {
  // These operations can happen in parallel
  const validateInventory = checkInventoryLevels(cartId);
  const processPayment = chargeCustomerCard(cartId);
  const reserveShipping = scheduleDelivery(cartId);
  
  return Promise.all([validateInventory, processPayment, reserveShipping])
    .then(([inventory, payment, shipping]) => {
      return {
        orderId: generateOrderId(),
        paymentId: payment.id,
        shippingTrackingCode: shipping.trackingCode,
        items: inventory.items
      };
    })
    .catch(error => {
      // If any step fails, the entire checkout fails
      cancelPendingOperations(cartId);
      throw new Error(`Checkout failed: ${error.message}`);
    });
}

Promise.race()

Promise.race() takes an iterable of promises and returns a new promise that resolves or rejects as soon as one of the promises resolves or rejects, with the value or reason from that promise.

Real-World Analogy

Promise.race() is like a competition where the first contestant to finish determines the result. Whether they win or lose (resolve or reject), their result becomes the final outcome.

Fast Promise Slow Promise Failed Promise Promise.race() First Result Takes the FIRST promise to settle (whether resolved or rejected) Finishes first
// Example: Race between fetching data and a timeout
function fetchWithTimeout(url, timeout = 5000) {
  // Create a promise that rejects after a timeout
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error(`Request to ${url} timed out after ${timeout}ms`));
    }, timeout);
  });
  
  // Race between the fetch and the timeout
  return Promise.race([
    fetch(url).then(response => response.json()),
    timeoutPromise
  ]);
}

// Using the function
fetchWithTimeout('/api/data', 3000)
  .then(data => {
    console.log('Data retrieved successfully:', data);
  })
  .catch(error => {
    console.error('Failed to fetch data:', error.message);
  });

// Real-world example: Fallback servers for high-availability
function fetchFromMirrors(resource) {
  const primaryServer = fetch(`https://primary.example.com/${resource}`);
  const backupServer = fetch(`https://backup.example.com/${resource}`);
  const failoverServer = fetch(`https://failover.example.com/${resource}`);
  
  // Return the first successful response
  return Promise.race([primaryServer, backupServer, failoverServer]);
}

Modern Promise Combinators

ES2020 and ES2021 added two important new static methods to the Promise API that give developers more control and flexibility when working with multiple promises.

Promise.allSettled() (ES2020)

Promise.allSettled() takes an iterable of promises and returns a new promise that resolves after all promises have settled (either resolved or rejected). The resolved value is an array of objects describing the outcome of each promise.

Real-World Analogy

Promise.allSettled() is like sending multiple scouts to explore different paths. You wait for all of them to return regardless of what they found, then analyze all the reports together.

// Example: Attempt to fetch multiple resources and report all results
const endpoints = [
  'https://api.example.com/users',
  'https://api.example.com/invalid-endpoint', // Will 404
  'https://api.example.com/products'
];

const requests = endpoints.map(url => 
  fetch(url)
    .then(response => response.json())
    .catch(error => ({ error: error.message }))
);

Promise.allSettled(requests)
  .then(results => {
    // Process all results, including errors
    results.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        console.log(`Endpoint ${endpoints[index]} succeeded:`, result.value);
      } else {
        console.log(`Endpoint ${endpoints[index]} failed:`, result.reason);
      }
    });
    
    // Continue with whatever data we successfully retrieved
    const successfulResults = results
      .filter(result => result.status === 'fulfilled')
      .map(result => result.value);
    
    processAvailableData(successfulResults);
  });

// Real-world example: Batch operations with partial success
function batchProcessUsers(userIds) {
  const processingOperations = userIds.map(id => 
    processUserData(id)
      .then(result => ({ status: 'fulfilled', userId: id, result }))
      .catch(error => ({ status: 'rejected', userId: id, error }))
  );
  
  return Promise.allSettled(processingOperations)
    .then(results => {
      // Generate a comprehensive report
      const successful = results.filter(r => r.status === 'fulfilled');
      const failed = results.filter(r => r.status === 'rejected');
      
      return {
        totalProcessed: userIds.length,
        successCount: successful.length,
        failureCount: failed.length,
        successfulIds: successful.map(r => r.userId),
        failedIds: failed.map(r => r.userId),
        failures: failed.map(r => ({ id: r.userId, reason: r.error.message }))
      };
    });
}

Promise.any() (ES2021)

Promise.any() takes an iterable of promises and returns a new promise that resolves as soon as one of the promises resolves. If all promises reject, it rejects with an AggregateError containing all rejection reasons.

Real-World Analogy

Promise.any() is like a search party looking for something - you need just one person to find it for the mission to succeed. Only if everyone fails do you declare the mission a failure.

graph TD A["Promise.any([p1, p2, p3])"] --> B{{"Any promise resolves?"}} B -->|Yes| C["Resolve with first
successful value"] B -->|No| D["Reject with
AggregateError"] style A fill:#d0e8ff,stroke:#333,stroke-width:1px style B fill:#ffe0b2,stroke:#333,stroke-width:1px style C fill:#c8e6c9,stroke:#333,stroke-width:1px style D fill:#ffcdd2,stroke:#333,stroke-width:1px
// Example: Try multiple image CDNs until one works
function loadImageFromMirrors(filename) {
  const mirrors = [
    `https://cdn1.example.com/images/${filename}`,
    `https://cdn2.example.com/images/${filename}`,
    `https://cdn3.example.com/images/${filename}`
  ];
  
  const imagePromises = mirrors.map(url => {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(url);
      img.onerror = () => reject(`Failed to load from ${url}`);
      img.src = url;
    });
  });
  
  return Promise.any(imagePromises)
    .then(successfulUrl => {
      console.log(`Image loaded from: ${successfulUrl}`);
      return successfulUrl;
    })
    .catch(errors => {
      // AggregateError contains all the individual errors
      console.error('All image sources failed');
      throw new Error('Could not load image from any source');
    });
}

// Real-world example: Authentication with multiple providers
function authenticateUser(credentials) {
  // Try multiple auth services in parallel
  const primaryAuth = authenticateWithPrimary(credentials);
  const legacyAuth = authenticateWithLegacy(credentials);
  const federatedAuth = authenticateWithFederated(credentials);
  
  return Promise.any([primaryAuth, legacyAuth, federatedAuth])
    .then(authResult => {
      // Use the first successful authentication
      return {
        authenticated: true,
        token: authResult.token,
        provider: authResult.provider
      };
    })
    .catch(errors => {
      // All authentication methods failed
      console.error('Authentication failed with all providers');
      return {
        authenticated: false,
        reason: 'All authentication methods failed'
      };
    });
}

Comparison of Promise Combinators

Let's compare the behavior of the different Promise combinators with a consistent example to highlight their differences.

Method Success Condition Failure Condition Result Value Best Use Case
Promise.all() All promises resolve Any promise rejects Array of all resolved values When you need all operations to succeed
Promise.race() First promise settles First promise settles (if rejected) Value/reason of first settled promise Timeouts, cancelation, first-response-wins
Promise.allSettled() Always succeeds Never fails Array of result objects with status When you need results from all operations regardless of success/failure
Promise.any() Any promise resolves All promises reject First resolved value When you need at least one operation to succeed
// Example to show the differences
const promises = [
  Promise.resolve('Success 1'),
  Promise.reject('Error 1'),
  new Promise(resolve => setTimeout(() => resolve('Success 2'), 1000)),
  new Promise((_, reject) => setTimeout(() => reject('Error 2'), 1500))
];

// Promise.all
Promise.all(promises)
  .then(results => console.log('all success:', results))
  .catch(error => console.log('all error:', error));
// Output: "all error: Error 1" (fails fast)

// Promise.race
Promise.race(promises)
  .then(result => console.log('race success:', result))
  .catch(error => console.log('race error:', error));
// Output: "race success: Success 1" (first promise resolves)

// Promise.allSettled
Promise.allSettled(promises)
  .then(results => console.log('allSettled:', results));
// Output: "allSettled: [
//   {status: 'fulfilled', value: 'Success 1'},
//   {status: 'rejected', reason: 'Error 1'},
//   {status: 'fulfilled', value: 'Success 2'},
//   {status: 'rejected', reason: 'Error 2'}
// ]"

// Promise.any
Promise.any(promises)
  .then(result => console.log('any success:', result))
  .catch(errors => console.log('any error:', errors));
// Output: "any success: Success 1" (succeeds with first success)

Practical Applications & Patterns

Sequential vs. Parallel Promise Execution

Understanding when to use sequential promise execution (one after another) versus parallel execution (all at once) is crucial for performance and correctness.

flowchart LR subgraph Sequential A1["Promise 1"] --> A2["Promise 2"] --> A3["Promise 3"] end subgraph Parallel B1["Promise 1"] --> B4["Results"] B2["Promise 2"] --> B4 B3["Promise 3"] --> B4 end style Sequential fill:#e3f2fd,stroke:#333,stroke-width:1px style Parallel fill:#f1f8e9,stroke:#333,stroke-width:1px
// Sequential execution (when order matters)
async function processFilesSequentially(fileIds) {
  const results = [];
  
  for (const id of fileIds) {
    // Each operation waits for the previous one
    const file = await fetchFile(id);
    const processed = await processFile(file);
    results.push(processed);
  }
  
  return results;
}

// Parallel execution (when operations can happen simultaneously)
async function processFilesInParallel(fileIds) {
  // Create all promises at once
  const fetchPromises = fileIds.map(id => fetchFile(id));
  
  // Wait for all fetches to complete
  const files = await Promise.all(fetchPromises);
  
  // Process all files in parallel
  const processPromises = files.map(file => processFile(file));
  
  // Wait for all processing to complete
  return Promise.all(processPromises);
}

Controlling Concurrency

Sometimes you want to run operations in parallel, but with a limit on how many can run simultaneously to avoid overwhelming resources.

// Function to process items with limited concurrency
async function processWithConcurrencyLimit(items, concurrencyLimit, processItem) {
  const results = [];
  const running = new Set();
  
  // Process all items
  for (const item of items) {
    // Create a promise for this item
    const promise = processItem(item).then(result => {
      running.delete(promise);
      return result;
    });
    
    // Add it to the set of running promises
    running.add(promise);
    results.push(promise);
    
    // If we've hit the concurrency limit, wait for one to finish
    if (running.size >= concurrencyLimit) {
      await Promise.race(running);
    }
  }
  
  // Wait for all remaining promises to settle
  return Promise.all(results);
}

// Usage example: Process 100 images with max 5 concurrent operations
const imageIds = Array.from({ length: 100 }, (_, i) => `img_${i}`);

processWithConcurrencyLimit(imageIds, 5, async (imageId) => {
  const image = await fetchImage(imageId);
  const processed = await applyFilters(image);
  return processed;
}).then(results => {
  console.log(`Processed ${results.length} images`);
});

Error Recovery Strategies

Using Promise combinators effectively for different error handling scenarios is a key skill for robust asynchronous code.

// Retry a failed operation with exponential backoff
async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 300) {
  let retries = 0;
  
  while (true) {
    try {
      return await operation();
    } catch (error) {
      if (retries >= maxRetries) {
        throw error; // Max retries reached, propagate the error
      }
      
      // Calculate delay with exponential backoff and jitter
      const delay = baseDelay * Math.pow(2, retries) + Math.random() * 100;
      console.log(`Retry ${retries + 1} after ${delay.toFixed(0)}ms`);
      
      // Wait before retrying
      await new Promise(resolve => setTimeout(resolve, delay));
      retries++;
    }
  }
}

// Usage example
retryWithBackoff(() => fetchUserData(userId))
  .then(data => {
    console.log('Successfully retrieved user data:', data);
  })
  .catch(error => {
    console.error('Failed after multiple retries:', error);
  });

// Fallback chain with multiple options
function fetchWithFallbacks(resourceId) {
  const primaryPromise = fetchFromPrimary(resourceId);
  const cachePromise = primaryPromise.catch(() => fetchFromCache(resourceId));
  const backupPromise = cachePromise.catch(() => fetchFromBackup(resourceId));
  const defaultPromise = backupPromise.catch(() => getDefaultResource(resourceId));
  
  return defaultPromise;
}

Browser Support and Polyfills

Modern browsers support all Promise static methods, but some older browsers may not support the newer methods like Promise.allSettled() or Promise.any().

For older browsers, you can use polyfills or implement these methods yourself:

// Simple polyfill for Promise.allSettled
if (!Promise.allSettled) {
  Promise.allSettled = function(promises) {
    return Promise.all(
      promises.map(p => 
        Promise.resolve(p)
          .then(value => ({ status: 'fulfilled', value }))
          .catch(reason => ({ status: 'rejected', reason }))
      )
    );
  };
}

// Simple polyfill for Promise.any
if (!Promise.any) {
  Promise.any = function(promises) {
    return new Promise((resolve, reject) => {
      let errors = [];
      let rejectedCount = 0;
      const totalCount = promises.length;
      
      if (totalCount === 0) {
        reject(new AggregateError([], 'No promises to resolve'));
        return;
      }
      
      promises.forEach((promise, index) => {
        Promise.resolve(promise).then(
          value => {
            resolve(value); // Resolve with the first success
          },
          error => {
            errors[index] = error;
            rejectedCount++;
            
            // If all promises rejected, reject with AggregateError
            if (rejectedCount === totalCount) {
              reject(new AggregateError(errors, 'All promises rejected'));
            }
          }
        );
      });
    });
  };
}

Best Practices & Common Pitfalls

Best Practices

Common Pitfalls

// Pitfall: Not returning promises in chain
fetchUser(userId)
  .then(user => {
    // Missing return, breaks the chain!
    processUserData(user);
  })
  .then(processedData => {
    // processedData will be undefined!
    displayUserProfile(processedData);
  });

// Correct version
fetchUser(userId)
  .then(user => {
    // Properly return the next promise
    return processUserData(user);
  })
  .then(processedData => {
    displayUserProfile(processedData);
  });

// Pitfall: Not considering the behavior of Promise.all with errors
Promise.all([reliable(), mightFail(), alsoReliable()])
  .then(results => {
    // If mightFail() rejects, this never executes
    // and alsoReliable()'s result is lost
    processAllResults(results);
  });

// Better approach for operations that might fail
Promise.allSettled([reliable(), mightFail(), alsoReliable()])
  .then(results => {
    // Process all results, handling failures individually
    const successResults = results
      .filter(r => r.status === 'fulfilled')
      .map(r => r.value);
    
    processAvailableResults(successResults);
  });

Practice Exercises

Exercise 1: Sequential Data Processing

Create a function that processes an array of user IDs sequentially, fetching each user's data and processing it before moving to the next. Use Promise.resolve() to handle both synchronous and asynchronous operations.

Exercise 2: Parallel Data Fetching with Timeout

Implement a function that fetches data from multiple APIs in parallel but adds a timeout for each request. If any request takes too long, it should be canceled and replaced with a default value.

Exercise 3: Race Condition Handling

Create a function that loads an image from multiple CDNs using Promise.any(). Add proper error handling for cases where all CDNs fail.

Exercise 4: Batch Processing with Status Reporting

Implement a batch processing system that processes items in small batches to avoid overloading resources. Use Promise.allSettled() to report on the success or failure of each item.

Summary

Promise static methods provide powerful tools for coordinating asynchronous operations in JavaScript:

By understanding these methods and their use cases, you can create more efficient, resilient, and maintainable asynchronous code. Modern JavaScript applications frequently use these techniques to handle complex workflows involving multiple data sources, parallel operations, and error recovery mechanisms.

Further Reading