It's 2026 — why does await inside forEach send all requests before waiting for results?
In 2026, when doing batch data backfill, await inside forEach does not make the loop wait. The root cause is not that await fails; it is that forEach never consumes the Promise returned by the callback. It invokes each async callback as if it were a synchronous function, the Promise returned inside the callback has no consumer, and the loop naturally continues first. To make the loop actually wait for async work: use for…of when you need sequential order; use map with Promise.all when order does not matter and concurrency is acceptable; when the server has rate limits, split into batches, run in parallel within a batch, and wait between batches. Choosing the wrong channel commonly costs you disordered order, API rate limiting, or incomplete page data.
Why await inside forEach seems not to work
To understand this, hold on to one distinction: a function being called happens synchronously; an async task completing happens in the future. As soon as an async callback is called, its internal code runs to the first await and then returns a Promise; when that Promise settles and whether it fails depends on whether anyone consumes it. The implementation of forEach does not await and does not look at the return value, so every round is started, but no round is waited for.
There are two related consequences: errors thrown inside an async callback become a rejected Promise, forEach cannot catch it, and this easily triggers unhandledrejection — the console reports an error while the business flow keeps running; an await written inside a forEach callback is syntactically legal, the editor does not warn, and runtime does not error, so it often only surfaces during integration testing or after release. When troubleshooting, it is easy to suspect the API and network first, when the real issue is the wrong iteration method.
- forEach returns undefined, and a return inside the callback does not participate in control flow.
- break and continue are both unavailable in forEach; to stop midway, switch to for with break, or use some / every.
- An async callback always returns a Promise, even if no return is written, which is often overlooked.
Sequential or concurrent: four common channels in 2026
Split async array iteration into four channels. The split is based on only two questions: do you need to guarantee execution order, and do you need to limit the number of concurrent operations at the same moment. Once order and concurrency are decided, the implementation is basically decided.
- Sequential waiting: for…of with await. Each round waits for the previous round to finish before starting the next; order is deterministic, and total time is roughly the sum of each item.
- Parallel waiting: map with Promise.all. All tasks are sent first, then results are awaited together; total time is close to the longest single item.
- Parallel fault tolerance: map with Promise.allSettled. Concurrent behavior is the same as above, but one failure does not interrupt the whole batch; suitable for batch operations where partial failure is acceptable.
- Streaming consumption: for await…of. Used for async iterators, paginated fetching, or streaming responses; process as data arrives, without accumulating all data in memory first.
A suggested order of judgment: first ask whether the tasks depend on each other; if they do, you can only go sequential. If they do not, ask how much concurrency the server can handle; if it can handle it, go parallel; if not, go parallel with batching. Only when the data itself arrives as a stream does streaming consumption come into play. If you pick an implementation first and think about semantics afterward, the probability of rework is higher.
From the delivery floor: why batch backfill is easily rate-limited
Once, an order list needed logistics information backfilled item by item. The constraints were: the API documentation stated limited concurrency per account, a typical range for a single response was 150–400ms, and the page still needed loading and failure retry. The initial implementation was forEach with await, intended to be sequential, but dozens of requests were fired at the same moment, the server returned a batch of 429s due to rate limiting, some rows were simply left blank, and the integration session was halted.
The fix was to split the array into batches of 3–8, use Promise.all within a batch and for…of to wait between batches, then add per-item failure retry and a skeleton screen. It cost roughly an extra half day to a day of work; most rework went into checking the API rate-limit rules and filling in exception states. When delivering this kind of list page, I put 'does one loop need concurrency control' into the integration checklist, because unlike styling issues it is not visible at a glance and often only surfaces during load testing or production alerts. Check the API's concurrency capacity and timeout settings before deciding the loop implementation; that is less trouble than writing it and changing it afterward.
A checkable comparison of sequential, parallel, and batched
Put several typical paths side by side. The difference is mainly in the time curve and instantaneous pressure, not in lines of code.
- for…of sequential: 20 items, about 100–300ms each, total time experience range roughly 2–6 seconds; instantaneous concurrency is 1, server pressure is low, order is controllable; one failure interrupts the rest, so you need your own try/catch and retry.
- map + Promise.all parallel: the same 20 items, total time usually drops to near the longest item, commonly a few hundred milliseconds to just over 1 second; instantaneous concurrency is 20, and both server and browser connection counts can be strained; one failure rejects the whole batch.
- Batched parallel: commonly split into batches of 3–10, with both time and pressure in the middle; a common compromise for list backfill scenarios, with batch size adjusted per API documentation or actual measurement.
- Pure local computation: when no network is involved, there is no need to introduce async iteration; a plain for loop is more direct and has no extra Promise overhead.
The rule of thumb can be simplified to one sentence: if the next item needs the previous result to be computed, go sequential; if you only need all results before rendering together, go parallel or batched. Forcing the opposite usually costs longer first-screen waits, or incomplete page data after the server triggers rate limiting.
Applicable boundaries and inapplicable boundaries
This judgment fits batch processing with network requests where order or concurrency requirements exist: list backfill, batch submission, paginated fetching, streaming reception. Conversely, there are clear non-use scenarios.
- Suitable: batch independent queries; item-by-item dependencies; paginated or streaming data; the need to actively control concurrency to protect the server.
- Not suitable: pure synchronous array computation, where a plain for is more direct; tracking/analytics reporting that only fires and does not care about results, where forcing sequential waiting actually slows the main flow.
- Can be postponed: scenarios with single-digit data volume and a fast API, where introducing a batching framework just for a unified style is usually not worth it.
Remember the boundary sentence like this: await in array iteration is only responsible for the loop you write yourself; array methods such as forEach, map, and filter will not wait for you. Whenever you need to wait, you must explicitly choose sequential or parallel.
FAQ
Does writing await inside forEach throw an error?
No. It is syntactically legal; the async callback returns a Promise, and forEach neither receives nor waits for it, so there is no error and no effect — the observable behavior is that later code runs first.
Is await inside for…of always sequential?
Yes. Each round waits for the previous round to finish before starting the next, so order is deterministic; with 20 items at 200ms each, the total is about 4 seconds, which is longer overall.
If I want parallelism but also a concurrency limit, is there a simple approach?
Split the array into batches of 3–10, use Promise.all within a batch and for…of to wait between batches; this typical range balances speed and server pressure.
Inside iteration, how do I choose between Promise.all and allSettled?
Use all when everything must succeed before continuing; use allSettled when partial failure is allowed and you still need per-item results, then handle the failed items together.
Each await in the loop calls an API; how do I reduce the number of calls?
First make a batch API or use caching to merge duplicate parameters; next consider batched concurrency. Adding await inside the loop without changing the API will not reduce the number of calls.
In 2026, when you again encounter 'the loop does not wait for async work', first confirm whether the callback is attached to forEach or map, then decide whether this batch of tasks needs order. If order is needed, go for…of sequential; if not, go batched parallel, and write failure retry and loading into the acceptance items together. For small data volumes and fast APIs, there is no need to force in a batching framework; when the API has rate limits, check the concurrency limit per platform documentation first, then decide the batch size.
-
An H5 campaign page embedded in Flutter scrolls rough on Android—which layer should you touch first?
Date: Oct 14, 2026 Read: 5
-
It's 2026 and the page title isn't in the source code — where does Baidu get the indexed title?
Date: Oct 13, 2026 Read: 9
-
OnPush child component not refreshing with polling data in Angular: replace the array reference first, or add markForCheck first?
Date: Oct 12, 2026 Read: 14
-
It's 2026, two dialogs in Vue share the same composable—one fails to submit and the other also shows an error. Is state leaking?
Date: Oct 11, 2026 Read: 32
-
In 2026, why do sticky table headers on list pages stop sticking halfway through scrolling? Do you really need to change every parent overflow?
Date: Oct 8, 2026 Read: 29




