Mastering Concurrent API Calls in Node.js for Resilience

Learn to handle multiple external API calls concurrently in Node.js, ensuring high performance, fault tolerance, and a robust user experience, even with service failures.

T

TechScale

October 14, 2026 13 min read
Share:
Mastering Concurrent API Calls in Node.js for Resilience

How to Handle Multiple External API Calls Concurrently in a Node.js Application

Modern web applications often rely on data from various external services. For instance, a content details page might need to fetch content metadata, user profile information, viewing history, and favorite status from separate microservices. When building a Node.js backend, orchestrating these calls efficiently and robustly is paramount.

The Challenge of Concurrent API Calls

The core problem lies in retrieving all necessary data for a single frontend request while maintaining performance and handling potential failures from any of the dependent services. Without proper concurrency and error handling, a slow or failing service can block the entire response, leading to poor user experience and application instability.

Why This Matters

  • Performance: Sequential calls are slow. Concurrency reduces overall latency by initiating requests in parallel.
  • User Experience: Even if one service fails, returning partial data can keep the user engaged (e.g., showing content details even if watch history isn't available).
  • Resilience: Applications must be resilient to transient network issues, slow responses, and service outages.

In this deep dive, we'll explore how to effectively manage multiple API calls concurrently in Node.js, using Promise.all for strict success requirements and Promise.allSettled for scenarios where partial success is acceptable and desired.

Node.js API Concurrent Call Architecture
Rendering architectural diagram...
Architectural flow of a Node.js API handling concurrent calls to multiple external services, demonstrating potential slow responses and errors.

Solution 1: Promise.all (All or Nothing Approach)

Promise.all is a fundamental method in JavaScript for handling multiple promises concurrently. It takes an iterable of promises (e.g., an array) and returns a single Promise that fulfills when all of the input promises have fulfilled, or rejects as soon as any of the input promises rejects.

This approach is suitable when all the data from the external services is considered mandatory. If even one piece of data fails to load, the entire operation is deemed a failure, and an error is returned to the client.

How it Works

  1. Concurrent Execution: All promises passed to Promise.all start executing more or less simultaneously.
  2. Order Preservation: The order of the results array matches the order of the promises in the input iterable.
  3. Fail Fast: If any promise in the array rejects, Promise.all immediately rejects with the reason of the first promise that rejected, ignoring any other pending or resolved promises.
src/api/contentPageAll.ts
typescript
interface ContentPageDataAll { content: string; profile: string; watchHistory: string; favoriteStatus: string; }

// Placeholder functions simulating API calls
const getContentMetadataAll = (): Promise<string> => new Promise(resolve => setTimeout(() => resolve("Content metadata loaded"), 200));
const getUserProfileAll = (): Promise<string> => new Promise(resolve => setTimeout(() => resolve("User profile loaded"), 500));
const getWatchHistoryAll = (): Promise<string> => new Promise((_, reject) => setTimeout(() => reject(new Error("Watch history service error (5xx)")), 300)); // Simulate failure
const getFavoriteStatusAll = (): Promise<string> => new Promise(resolve => setTimeout(() => resolve("Favorite status loaded"), 100));

async function getContentPageAll(): Promise<ContentPageDataAll> {
  try {
    const [content, profile, watchHistory, favoriteStatus] = await Promise.all([
      getContentMetadataAll(),
      getUserProfileAll(),
      getWatchHistoryAll(), // This one will reject
      getFavoriteStatusAll()
    ]);
    return { content, profile, watchHistory, favoriteStatus };
  } catch (error: any) {
    console.error(`Failed to retrieve all data: ${error.message}`);
    throw new Error(`Critical data missing: ${error.message}`);
  }
}

// --- Test Cases ---
console.log('--- Testing Promise.all (All or Nothing) ---');
(async () => {
  try {
    const data = await getContentPageAll();
    console.log('Promise.all Success:', data);
  } catch (error: any) {
    console.error('Promise.all Failure:', error.message);
  }
})();

// Example of a successful Promise.all scenario
const getSuccessMetadata = (): Promise<string> => new Promise(resolve => setTimeout(() => resolve("Content metadata success"), 100));
const getSuccessProfile = (): Promise<string> => new Promise(resolve => setTimeout(() => resolve("User profile success"), 150));

console.log('--- Testing Promise.all (All Success) ---');
(async () => {
    try {
        const [meta, profile] = await Promise.all([getSuccessMetadata(), getSuccessProfile()]);
        console.log('Promise.all All Success:', { meta, profile });
    } catch (error: any) {
        console.error('Promise.all All Success Failed:', error.message);
    }
})();

Complexity Analysis

  • Time Complexity: O(M), where M is the time taken by the slowest executing promise. This is because all promises run in parallel, and Promise.all waits for the last one to complete.
  • Space Complexity: O(N), where N is the number of promises, for storing the promises and their results.

Pros and Cons

Pros:

  • Simple and straightforward to implement.
  • Guarantees that all required data is present if the operation succeeds.
  • "Fail fast" behavior can be desirable for critical data dependencies.

Cons:

  • A single slow or failing service will cause the entire Promise.all to reject, even if other services succeeded.
  • Not suitable for scenarios where partial data is acceptable or preferred for a better user experience.

Solution 2: Promise.allSettled (Partial Success Approach)

Promise.allSettled is designed for scenarios where you need to wait for all promises to settle (either fulfill or reject) and want to know the outcome of each individual promise, regardless of whether any failed. This is ideal when some data is essential while others are optional, or when you want to aggregate results even if some parts fail.

How it Works

  1. Concurrent Execution: Like Promise.all, all promises passed to Promise.allSettled execute concurrently.
  2. Waits for All: It waits for all input promises to finish, either by resolving successfully or rejecting with an error.
  3. Detailed Results: It returns a promise that resolves with an array of objects. Each object describes the outcome of an individual promise, containing a status property ('fulfilled' or 'rejected') and either a value (if fulfilled) or a reason (if rejected).

This allows you to gracefully handle individual failures, provide fallback values, and return as much data as possible to the client, improving resilience and user experience.

src/api/contentPageAllSettled.ts
typescript
interface ContentPageData { content: unknown; profile: unknown | null; watchHistory: unknown | null; favoriteStatus: unknown | null; }

// Placeholder functions simulating API calls with varying latencies and failures
const getContentMetadata = (): Promise<any> => new Promise(resolve => setTimeout(() => resolve({ id: 'meta-1', title: 'Article Title' }), 200));
const getUserProfile = (): Promise<any> => new Promise(resolve => setTimeout(() => resolve({ userId: 'user-abc', name: 'John Doe' }), 500)); // Simulating slow service
const getWatchHistory = (): Promise<any> => new Promise((_, reject) => setTimeout(() => reject(new Error("Viewing History Service Unavailable (5xx)")), 300)); // Simulating failure
const getFavoriteStatus = (): Promise<any> => new Promise(resolve => setTimeout(() => resolve({ isFavorite: true }), 100));

async function getContentPage(): Promise<ContentPageData> {
  const results = await Promise.allSettled([
    getContentMetadata(),
    getUserProfile(),
    getWatchHistory(),
    getFavoriteStatus()
  ]);

  // Essential data check: Content Metadata
  const contentResult = results[0];
  if (contentResult.status === "rejected") {
    throw new Error(`Unable to retrieve essential content metadata: ${contentResult.reason}`);
  }

  // Extracting data and handling optional failures
  const content = (contentResult as PromiseFulfilledResult<unknown>).value;
  const profile = results[1].status === "fulfilled" ? (results[1] as PromiseFulfilledResult<unknown>).value : null;
  const watchHistory = results[2].status === "fulfilled" ? (results[2] as PromiseFulfilledResult<unknown>).value : null;
  const favoriteStatus = results[3].status === "fulfilled" ? (results[3] as PromiseFulfilledResult<unknown>).value : null;

  return {
    content,
    profile,
    watchHistory,
    favoriteStatus
  };
}

// --- Test Cases ---
console.log('--- Testing Promise.allSettled (Partial Success) ---');
(async () => {
  try {
    const data = await getContentPage();
    console.log('Promise.allSettled Success with Partial Data:', JSON.stringify(data, null, 2));
  } catch (error: any) {
    console.error('Promise.allSettled Critical Failure:', error.message);
  }
})();

// Example of all services succeeding
const getSuccessMetadata2 = (): Promise<any> => new Promise(resolve => setTimeout(() => resolve({ id: 'meta-2', title: 'Another Article' }), 50));
const getSuccessProfile2 = (): Promise<any> => new Promise(resolve => setTimeout(() => resolve({ userId: 'user-xyz', name: 'Jane Doe' }), 70));
const getSuccessWatchHistory2 = (): Promise<any> => new Promise(resolve => setTimeout(() => resolve({ items: ['item-a', 'item-b'] }), 60));
const getSuccessFavoriteStatus2 = (): Promise<any> => new Promise(resolve => setTimeout(() => resolve({ isFavorite: false }), 40));

async function getContentPageAllSuccess(): Promise<ContentPageData> {
    const results = await Promise.allSettled([
        getSuccessMetadata2(),
        getSuccessProfile2(),
        getSuccessWatchHistory2(),
        getSuccessFavoriteStatus2()
    ]);

    const content = (results[0] as PromiseFulfilledResult<unknown>).value;
    const profile = (results[1] as PromiseFulfilledResult<unknown>).value;
    const watchHistory = (results[2] as PromiseFulfilledResult<unknown>).value;
    const favoriteStatus = (results[3] as PromiseFulfilledResult<unknown>).value;

    return { content, profile, watchHistory, favoriteStatus };
}

console.log('--- Testing Promise.allSettled (All Fulfilled) ---');
(async () => {
    try {
        const data = await getContentPageAllSuccess();
        console.log('Promise.allSettled All Fulfilled:', JSON.stringify(data, null, 2));
    } catch (error: any) {
        console.error('Promise.allSettled All Fulfilled Failed:', error.message);
    }
})();

Complexity Analysis

  • Time Complexity: O(M), where M is the time taken by the slowest executing promise. Similar to Promise.all, as all promises run in parallel.
  • Space Complexity: O(N), where N is the number of promises, for storing the promises and their detailed results.

Pros and Cons

Pros:

  • Allows for partial success, providing a better user experience even if some services fail.
  • Provides detailed status for each promise, making error handling granular.
  • More robust against individual service failures.

Cons:

  • Requires more boilerplate code to parse the results and handle individual statuses (fulfilled or rejected).
  • Always waits for all promises to settle, even if a critical one failed early.

Step-by-Step Execution Trace (Scenario with Partial Failure)

Let's trace the execution for fetching content details when the Viewing History Service experiences a 5xx error and Profile Service is slow.

StepActionService/PromiseDuration/StatusOutcome
1Frontend requests page--Request for /content-details received
2Node.js API initiates calls-ConcurrentAll promises for services launched in parallel
3Fetch Content MetadataCatalog Service200ms, Fulfilledcontent data retrieved successfully
4Fetch User ProfileProfile Service5s, Fulfilled (slow)profile data retrieved successfully
5Fetch Watch HistoryViewing History Service500ms, RejectedwatchHistory call fails (5xx error)
6Fetch Favorite StatusPreferences Service300ms, FulfilledfavoriteStatus retrieved successfully
7Promise.allSettled waits-All settled (5s)All individual promises complete
8Node.js API aggregates--content, profile, favoriteStatus are extracted, watchHistory is null
9Respond to Frontend--Partial JSON response sent to client

Example Partial Response to Frontend

json
json
{
  "content": { "id": "meta-1", "title": "Article Title" },
  "profile": { "userId": "user-abc", "name": "John Doe" },
  "watchHistory": null, // Service failed
  "favoriteStatus": { "isFavorite": true }
}

In this scenario, despite the Viewing History Service failure and the Profile Service being slow, the Node.js API successfully returned essential content and user profile data, demonstrating the power of Promise.allSettled for partial success.

Comprehensive Comparison: Promise.all vs. Promise.allSettled

Choosing between Promise.all and Promise.allSettled depends on the criticality of the data being fetched and your desired error handling strategy.

FeaturePromise.allPromise.allSettled
Use CaseAll data is mandatory; fail if any call fails.Some data is optional; prioritize partial success.
Error Handling"Fail fast": Rejects on the first error."Never rejects": Resolves with individual statuses for all promises.
Return ValueAn array of resolved values (if all succeed).An array of objects, each with status ('fulfilled' or 'rejected'), value, or reason.
Data CriticalityHigh: All data is essential for the operation.Mixed: Essential and optional data.
ProsSimple, guarantees full data, good for critical paths.Robust, partial success, granular error details.
ConsSingle point of failure, entire operation aborts.More boilerplate for result parsing.

Important Edge Cases & Handling for Robust API Integrations

Beyond basic concurrency, building resilient API integrations requires addressing several common challenges:

1. Slow Service Responses

A service might be slow but eventually respond. This can still degrade user experience. Implement timeouts for individual API calls.

2. Service Errors (e.g., 5xx)

External services can be temporarily unavailable or return unexpected errors. Strategies like bounded retries and fallback mechanisms are crucial.

3. Non-Essential Data

Some data (like watch history or personalized recommendations) might not be critical for the core functionality. In such cases, the system should still function and return available essential data.

4. Need for Resilience

Robust applications incorporate resilience patterns like timeouts, retries, circuit breakers, and comprehensive logging.

Here's an example demonstrating a fetchWithTimeout helper for individual API calls:

src/utils/fetchWithTimeout.ts
typescript
interface FetchOptions extends RequestInit {
  timeout?: number;
}

async function fetchWithTimeout(url: string, options: FetchOptions = {}): Promise<Response> {
  const { timeout = 5000, ...restOptions } = options;
  
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      ...restOptions,
      signal: controller.signal
    });
    clearTimeout(id);
    return response;
  } catch (error: any) {
    clearTimeout(id);
    if (error.name === 'AbortError') {
      throw new Error(`Request to ${url} timed out after ${timeout}ms`);
    }
    throw error;
  }
}

// --- Test Cases ---
console.log('--- Testing fetchWithTimeout ---');

// 1. Simulate a fast API call
(async () => {
  try {
    const response = await fetchWithTimeout('https://jsonplaceholder.typicode.com/todos/1', { timeout: 10000 });
    const data = await response.json();
    console.log('Fast API call (Success):', data.title);
  } catch (error: any) {
    console.error('Fast API call (Error):', error.message);
  }
})();

// 2. Simulate a slow API call that will timeout
(async () => {
  try {
    // This URL simulates a delay
    const response = await fetchWithTimeout('https://httpbin.org/delay/6', { timeout: 3000 }); 
    const data = await response.json();
    console.log('Slow API call (Success):', data);
  } catch (error: any) {
    console.error('Slow API call (Error):', error.message);
  }
})();

// 3. Simulate an immediate error
(async () => {
  try {
    // Invalid URL to trigger a network error quickly
    await fetchWithTimeout('http://invalid.url', { timeout: 5000 }); 
  } catch (error: any) {
    console.error('Immediate Error (Expected):', error.message.includes('Failed to fetch') ? 'Network error' : error.message);
  }
})();

Additional Error Handling Features (as seen in Section 6 of the infographic):

  • Bounded Retries: Instead of failing immediately, retry transient errors (e.g., 5xx, network issues) a fixed number of times with exponential backoff.
  • Fallback Behavior: Define default values or alternative logic if a non-essential service fails.
  • Structured Logging: Log API call details (request, response, errors, correlation IDs) for debugging and monitoring.
  • Monitoring & Alerts: Implement metrics and alerting for API latencies, error rates, and availability to proactively identify issues.

Key Takeaways for Robust API Integrations

When orchestrating external API calls in Node.js, always remember these architectural principles: * **Partial Responses are a Business Decision:** Don't silently hide failures. Understand which data is essential and which can be omitted. * **Implement Resilience:** Proactively apply timeouts, retries, and potentially circuit breakers to prevent cascading failures. * **Embrace Observability:** Implement comprehensive logging, metrics, and alerts for external API integrations to quickly detect and diagnose problems. * **Design for User Experience:** Even in failure scenarios, aim to return as much usable data as possible to keep the page usable and responsive.

Enjoyed this deep dive?

Share:
Tags:Node.jsAPIConcurrencyPromisesError HandlingResilience

Discussion & Feedback

0 thoughts shared by the community

💬

Loading discussion thread...