Back to Blog
Node.jsJuly 9, 20263 min read

Node.js Promise.allSettled: Handling Concurrent Async Tasks in Express

Master Node.js concurrent requests with Promise.allSettled in Express. Learn to handle partial failures and keep your API robust during async tasks.

Node.jsExpressJavaScriptAsyncPerformanceBackend

When you're building high-performance APIs in Node.js, you'll eventually hit a wall where a single route needs to aggregate data from three or four different microservices or database tables. If you use Promise.all for this, one service timing out or throwing a 500 effectively nukes the entire request.

I remember debugging a dashboard endpoint that aggregated user profile data, recent orders, and notification settings. We used Promise.all because it felt clean. But when the notification service had a transient blip, the whole dashboard failed for the user. It was a classic "all or nothing" trap.

Why Promise.allSettled is the Better Choice

Promise.allSettled solves the partial success problem. Unlike Promise.all, which short-circuits on the first rejection, allSettled waits for every promise to finish, regardless of whether it succeeded or failed. You get an array of result objects back, each clearly stating its status as either fulfilled or rejected.

This pattern is essential for Node.js async error handling in Express: a practical guide because it allows you to return partial data to the client rather than a generic error message.

Implementing the Pattern in Express

Let's look at a concrete example. Imagine an endpoint that fetches data from three external APIs.

JAVASCRIPT
const express = require(CE9178">'express');
const app = express();

app.get(CE9178">'/user-dashboard', async (req, res) => {
  const userId = req.query.id;

  // Fire off all requests concurrently
  const results = await Promise.allSettled([
    fetchProfile(userId),
    fetchOrders(userId),
    fetchNotifications(userId)
  ]);

  // Map the results to a clean response
  const response = {
    profile: results[0].status === CE9178">'fulfilled' ? results[0].value : null,
    orders: results[1].status === CE9178">'fulfilled' ? results[1].value : [],
    notifications: results[2].status === CE9178">'fulfilled' ? results[2].value : [],
    errors: results
      .filter(r => r.status === CE9178">'rejected')
      .map(r => r.reason.message)
  };

  res.json(response);
});

Handling the Trade-offs

You might be tempted to just use Promise.all and wrap each individual call in a try/catch. I’ve been there. It works, but the code gets incredibly verbose very quickly.

FeaturePromise.allPromise.allSettled
Fail-fastYes (stops at first error)No (waits for all)
Result TypeArray of valuesArray of {status, value/reason}
Best Use CaseDependent tasksIndependent concurrent tasks
Error HandlingRequires individual catchHandled via status check

Using allSettled keeps the controller logic flat. If you need to manage these transient failures with retries, I highly recommend checking out my post on the Node.js retry pattern: handling transient failures with Express.

A Note on Production Readiness

When using this pattern, always ensure your individual task functions are wrapped properly. If you leave an unhandled rejection, you're going to run into the issues I outlined in Node.js security: fixing unhandled promise rejections and async errors.

One thing I'm still careful about is memory usage. If you're firing off 50+ concurrent requests, you might want to look into using a concurrency-limiting library like p-limit. Node.js is excellent at handling thousands of connections, but you don't want to overwhelm your downstream services.

FAQ

Q: Does Promise.allSettled catch syntax errors in my code? A: No. It only catches rejections of the promises you pass into it. If your code throws a synchronous error, it will still bubble up to your Express error-handling middleware.

Q: Can I use this for database queries? A: Absolutely. Most modern ORMs like Prisma or native drivers return promises, making them perfect candidates for Promise.allSettled.

Q: How do I handle a scenario where one failure is critical? A: You can inspect the results array immediately after the await. If a critical service (like Auth) fails, you can manually trigger an error response even if other promises succeeded.

I've found that moving to allSettled makes my APIs significantly more resilient to the "noisy neighbor" effect of microservices. It's a small change in syntax that makes a massive difference in user experience. Next time, I'm going to experiment with streaming these results back to the client as they arrive, rather than waiting for the slowest one to finish.

Similar Posts