JavaScript example

How to use async/await in a loop in JavaScript

8 min read Updated Sep 2026 Runs in an isolated runtime
Quick answer

Don't use forEach: it ignores the promises its callback returns. To run the work one at a time, use for (const item of items) { await work(item); }. To run it all at once and wait for every result, use await Promise.all(items.map(work)). For a sleep(), wrap setTimeout in a promise: const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

await pauses only the async function it is written in. That one rule explains every surprise in this article. Put await inside a forEach callback and it pauses the callback, not the code that called forEach. Put it directly in a for...of body and the whole loop waits. The real decision is whether the work should happen one after another or at the same time, and what should happen when one piece fails. The examples fake slow work with a short sleep() instead of a network call, so the output is the same every run. Each one runs on this page: hit Run, then edit the code and run it again.

1A sleep() helper built from setTimeout

JavaScript has no built-in sleep(), because a function that blocked the thread would freeze everything else the program is doing. What you can do is make a promise that resolves after a delay and await it. setTimeout(resolve, ms) calls resolve once the time is up, which fulfils the promise and lets the awaiting code continue. Every other example on this page uses this one-line helper.

sleep.js

Output

Prints start and half a second later, then rocket 3 followed by the rest of the program keeps going, then rocket 2, rocket 1, rocket liftoff and end. The second half is the point. countdown ran until its first await and handed control back, so the next line printed before rocket 2. Nothing was blocked. The top-level await works because this code runs as an ES module. In an older CommonJS script, put the code inside an async function and call it.

2Why forEach does not wait

Here is the bug from the Stack Overflow question. An async callback returns a promise as soon as it reaches its first await. forEach calls the callback for each item, throws that promise away and moves on. It returns undefined, so there is nothing to await either: await items.forEach(...) waits for nothing. The three jobs below take 300, 100 and 200 ms.

foreach.js

Output

Prints forEach returned undefined and after forEach: [] first: the loop was over before any job finished. Then come finished b, finished c, finished a, and finally 500 ms later: [ 'b', 'c', 'a' ], in the order the timers fired rather than the order of the array. Two more problems hide here. The jobs all ran at once, which is not what most people who write await in a loop expect. And if a callback throws, nobody is listening: the rejection is unhandled, which crashes a Node process. The await sleep(500) guess only makes this demo work. Never use it as a fix. The same applies to map, filter, some and reduce with an async callback; none of them await it.

3One at a time: for...of with awaitRecommended

The direct fix is a real loop. In a for...of body, await pauses the enclosing function, so iteration two does not start until iteration one has finished. Use it when order matters, when each step depends on the one before, or when the other side can only handle one request at a time. A classic for (let i = 0; ...) loop and while work the same way; only the callback-based array methods don't.

sequential.js

Output

Prints loaded user3, loaded user1, loaded user2, then all done: [ 'user3', 'user1', 'user2' ], then 0 user3, 1 user1, 2 user2. User 3 is the slowest (150 ms) yet it still comes first, because nothing else starts until it is done. The cost is time: the loop takes the sum of all the delays. break, continue and return all work here, which they never do inside forEach, and a try/catch around the await catches a failure in that iteration.

4All at once: Promise.all with map

When the jobs are independent, start them all and wait once. items.map(work) calls the async function for every item straight away and returns an array of promises. Promise.all turns that array into one promise that fulfils with every result, in input order, once all of them are done. The total time is roughly the slowest job, not the sum.

parallel.js

Output

The first line is [ Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]. That is what map with an async callback gives you on its own, and why it always needs Promise.all. The jobs finish as finished b, finished c, finished a, but the result is still [ 'A', 'B', 'C' ], lined up with jobs. Then took under 400 ms: true: about 300 ms in total, where the for...of version would take 600. One catch: Promise.all rejects as soon as any promise rejects, which the next section deals with. It also starts everything at once, so for thousands of items, process them in batches (see the FAQ).

5When some fail: Promise.allSettled

With Promise.all, one failure throws away every result, including the ones that succeeded. Promise.allSettled never rejects. It waits for every promise and gives back one object per input, in input order: { status: 'fulfilled', value } or { status: 'rejected', reason }. Use it when a partial result is still useful, such as health checks, batch uploads or notifications.

all-settled.js

Output

Prints Promise.all threw: db timed out, and the cache and queue results are lost. The allSettled version prints the first outcome as { status: 'fulfilled', value: 'cache ok' }, then cache -> cache ok, db -> failed: db timed out and queue -> queue ok. Note that a rejected Promise.all does not cancel the other jobs; they keep running and their results are simply ignored. JavaScript promises can't be cancelled. If you want the first success instead of all of them, use Promise.any; for the first to finish either way, Promise.race.

6Values that arrive over time: for await...of

for await...of loops over an async iterable: a source that produces its next value later, such as pages from an API or chunks of a stream. The easiest way to write one is an async function* generator, which can await and yield. The loop asks for one value, waits for it, runs the body, then asks for the next.

for-await.js

Output

Prints got page: [ 'item1', 'item2' ], then [ 'item3', 'item4' ] and [ 'item5', 'item6' ] pages, then timer 300, timer 100, timer 200: array order, not the order they finished. Looping over an array of promises this way works, but prefer Promise.all for it. If a later promise rejects while the loop is still waiting on an earlier one, that rejection has no handler yet and can crash the process. for await is meant for sources that really produce values one by one, and it only works inside an async function or at the top level of a module.

7Which should you use?

MethodWaits for the workRunsBest for
for...of + awaitYesOne at a timeOrdered or dependent steps, rate limits
await Promise.all(items.map(fn))YesAll at onceIndependent jobs, all must succeed
await Promise.allSettled(…)YesAll at onceIndependent jobs where some may fail
for await...ofYesAs values arriveAsync generators, paged APIs, streams
reduce() promise chainYesOne at a timePre-2017 code; use for...of now
forEach(async …)NoAll at once, uncheckedNothing. Fire-and-forget at best

Frequently asked questions

Why doesn’t async/await work inside forEach?

It does run, but nothing waits for it. forEach calls your async callback, ignores the promise the callback returns, and returns undefined itself. So the code after forEach runs before any await inside it has finished, the jobs all start at once, and a rejection has no handler. Use for (const item of items) { await work(item); } for one at a time, or await Promise.all(items.map(work)) for all at once.

What is the JavaScript version of sleep()?

There is no blocking sleep(); the idiom is a promise that resolves after a delay: const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); then await sleep(1000); inside an async function or at the top level of a module. It pauses only the function that awaits it; other code keeps running. Node also ships this as setTimeout from node:timers/promises. Avoid busy-wait loops that spin on Date.now(): they freeze the whole thread.

Should I await in a loop or use Promise.all?

Use Promise.all when the jobs are independent: they run at the same time and the total is about as long as the slowest one. Use await in a for...of loop when each step needs the previous result, when order of side effects matters, or when the other side cannot take many requests at once. The sequential loop takes the sum of every delay, so do not use it for independent work out of habit.

How do I limit how many promises run at once?

The simplest way with plain JavaScript is batching: loop over the array in chunks with for (let i = 0; i < items.length; i += 5), and await Promise.all(items.slice(i, i + 5).map(work)) for each chunk. Each batch waits for its slowest job. For a steady pool of N workers, start N async loops that each take the next item from a shared index, and await Promise.all on the workers; the p-limit package wraps the same idea.

Can I use an async function with filter()?

Not directly. filter treats the returned promise as its answer, and every promise is truthy, so nums.filter(async (n) => n % 2 === 0) keeps every item. Compute the answers first with const keep = await Promise.all(nums.map(isEven)); and then filter by index: nums.filter((_, i) => keep[i]). The same applies to some, every and find.

Does Promise.all return results in order?

Yes. The result array lines up with the input array, whatever order the promises finished in. That is why await Promise.all(items.map(fn)) is safe to zip back with items by index. Promise.allSettled keeps input order too. Only side effects inside the jobs, such as log lines, happen in finish order.

Run it yourself

Open any of these in the full JavaScript editor: tweak, run, and share.

JavaScript playground