forEach does not wait for async callbacks
forEach ignores the promises your async callback returns, so the code after the loop runs before any of the awaits finish.
What it means
An async function returns a promise immediately. forEach calls your callback once per item, throws the returned promise away, and finishes — all synchronously. Nothing waits for the awaits inside. There is no error, which is what makes it dangerous: the work happens, just later than the code assumes.
Common causes
1. Saving items one by one, then reporting done
"Done" prints first, and any failures are unhandled rejections.
Breaks
items.forEach(async (item) => {
await save(item);
});
console.log("All saved");Works
for (const item of items) {
await save(item); // one at a time
}
console.log("All saved");2. Building a result inside an async forEach
The function returns the array before anything has been pushed into it.
Breaks
async function loadAll(ids) {
const users = [];
ids.forEach(async (id) => users.push(await getUser(id)));
return users; // []
}Works
async function loadAll(ids) {
return Promise.all(ids.map((id) => getUser(id)));
}3. The same trap with map
map does collect the promises, but you still have to await them.
Breaks
const results = items.map(async (x) => await check(x));
console.log(results); // [Promise, Promise, ...]Works
const results = await Promise.all(items.map((x) => check(x)));How to find it in your own code
Use for...of with await when the order matters or the calls must not overlap, and Promise.all(items.map(...)) when they can run at the same time. Never pass an async function to forEach.
Still not sure why yours breaks?
Paste it into the visualizer and watch it run line by line, with every variable at every step. Free, and it runs in your browser.