RSTR-PERF-101 — await inside a loop

Summary

A for / while loop body awaits a Promise. Each iteration blocks until the previous one resolves, so N independent async calls take the sum of their latencies instead of the maximum. The loop is effectively serial despite the async machinery.

Promise.all (or Promise.allSettled) parallelizes them.

Severity

Medium. The wall-clock difference is often 5×-20× for I/O loops.

Languages

JavaScript, TypeScript.

What rastray flags

const results = [];
for (const id of ids) {
    results.push(await fetch(`/items/${id}`));     // ← flagged
}
while (hasMore) {
    const page = await api.next();                  // ← flagged
    pages.push(page);
}

What rastray deliberately does not flag

  • Sequential awaits outside a loop.
  • Loops where the result of iteration N is the input to iteration N+1 (the loop is genuinely sequential by design).
  • Rate-limited iteration where serialization is intentional — suppress with a comment explaining the limit.

How to fix it

Map → Promise.all:

const results = await Promise.all(
    ids.map(id => fetch(`/items/${id}`))
);

For partial-failure tolerance:

const results = await Promise.allSettled(
    ids.map(id => fetch(`/items/${id}`))
);
const ok    = results.filter(r => r.status === 'fulfilled').map(r => r.value);
const fail  = results.filter(r => r.status === 'rejected');

For genuinely long lists, batch:

const out = [];
for (let i = 0; i < ids.length; i += 10) {
    const batch = ids.slice(i, i + 10);
    out.push(...await Promise.all(batch.map(id => fetch(`/items/${id}`))));
}

References