Mesrai
Back to blog
// essayTechnical Deep Dive

Promise Rejection Swallowed by Promise.allSettled Misuse

Real PR catch: allSettled result discarded — rejections invisible. Inspect + log + metric fix.

Mesrai TeamAugust 28, 20268 min read

A batch worker using `Promise.allSettled` then discarding the result. Half the items error out; nobody sees it. Mesrai flagged the misuse.

The vulnerable diff

tsworkers/batch.ts
// workers/batch.ts
async function batchProcess(items: Item[]) {
  // BUG: allSettled doesn't throw — result must be inspected
  await Promise.allSettled(items.map(item => processItem(item)));
}

What is wrong

`Promise.allSettled` is useful when you want to wait for every promise regardless of success/failure — unlike `Promise.all` which rejects on the first failure. But `allSettled` returns an array of `{ status, value | reason }`; the caller has to inspect it. Awaiting and discarding the result is functionally equivalent to writing a catch-all that swallows every rejection.

The attack

Symptom:

textexploit
batchProcess(items) called with 200 items.
50 of them error out (network blip, validation, etc).
The await resolves cleanly.
No logs, no metric, no signal.
Operator sees: 'batch complete'.

Subtle: looks like the right tool, used the wrong way.

Mesrai's review comment

textreview-comment.txt
mesraipilot · Bot · reviewed 1 min ago

[mesrai] [code-review] [Logic] [AllSettled-Misuse] [medium]

Promise.allSettled without inspecting the result swallows every
rejection. Inspect + decide:

  const results = await Promise.allSettled(
    items.map(item => processItem(item))
  );

  const failures = results
    .map((r, i) => ({ r, item: items[i] }))
    .filter(({ r }) => r.status === "rejected");

  if (failures.length > 0) {
    log.warn({ failed: failures.length, total: items.length }, "batch had failures");
    metrics.increment("batch.failed", failures.length);
    // optional: throw if you want batch-level failure
  }

If you don't actually need the all-settled semantics (you want fail-
fast), use Promise.all so errors propagate.

The fix

tsworkers/batch.ts (fixed)
// workers/batch.ts — fixed
async function batchProcess(items: Item[]) {
  const results = await Promise.allSettled(
    items.map(item => processItem(item))
  );
  const failures = results
    .map((r, i) => ({ r, item: items[i] }))
    .filter(({ r }) => r.status === "rejected");

  if (failures.length > 0) {
    log.warn({ failed: failures.length, total: items.length, samples: failures.slice(0, 3) },
             "batch had failures");
    metrics.increment("batch.failed", failures.length);
  }
}

Inspect the result, log failures, emit metric. Optional throw if batch-level failure should propagate.

Why human review missed it

Promise.allSettled is right for fault-tolerant batch work but the result must be consumed. Mesrai catches every `await Promise.allSettled` whose result is discarded.

Related rules + further reading

Mesrai rule pack: logic/all-settled-must-inspect — flags Promise.allSettled with discarded result.

MDN: Promise.allSettled — return value shape.

Common in batch workers that started as Promise.all and got switched.

Takeaway

allSettled returns; caller must inspect. Otherwise rejections vanish. Mesrai catches every discarded result.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.