Question
Using async/await with forEach in JavaScript
Question
I am using async/await inside an Array.prototype.forEach() loop in JavaScript and want to know whether this is safe.
I am looping through an array of file paths and awaiting the contents of each file:
import fs from 'fs-promise';
async function printFiles() {
const files = await getFilePaths(); // Assume this works correctly
files.forEach(async (file) => {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
});
}
printFiles();
This appears to work, but could it cause problems?
I was told that async/await should not be used this way inside a higher-order function like forEach(). I would like to understand whether there is an issue here, and if so, what the correct pattern should be.
Short Answer
By the end of this page, you will understand why async/await inside forEach() is often misleading in JavaScript, what can go wrong, and which alternatives to use instead. You will learn when to use for...of for sequential async work and when to use Promise.all() for parallel async work.
Concept
async/await works with promises, but forEach() does not know how to wait for promises returned by its callback.
That is the key issue.
When you write this:
files.forEach(async (file) => {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
});
forEach() starts each callback, but it does not:
- wait for one callback to finish before starting the next
- return a promise you can
await - handle async errors in the way beginners often expect
So although the code may appear to work, printFiles() can finish before all files are read.
That matters because in real programs you often need one of these behaviors:
- Sequential processing: read files one by one in order
- Parallel processing: read all files at the same time, then wait for all results
- Reliable error handling: catch failures predictably
forEach() is designed for synchronous iteration. It is great for running a normal function on each item, but it is not a good control-flow tool for asynchronous tasks.
Mental Model
Think of forEach() like a manager who quickly hands out tasks to workers and then immediately walks away.
If each worker does synchronous work, that is fine.
But if each worker says, "I will finish later," the manager does not stay around to check. forEach() does not wait for those delayed tasks.
By contrast:
for...ofis like a manager who waits for each worker to finish before assigning the next task.Promise.all()is like giving tasks to all workers at once, then waiting until everyone reports back.
So the real question is not "Can I put await inside forEach()?" You technically can. The real question is: Will the outer code wait the way I expect? With forEach(), the answer is usually no.
Syntax and Examples
Problematic pattern
files.forEach(async (file) => {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
});
This creates async callbacks, but forEach() does not wait for them.
Correct pattern for sequential processing
Use for...of when order matters or when you want to process one item at a time.
import fs from 'fs/promises';
async function printFiles() {
const files = await getFilePaths();
for (const file of files) {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
}
}
Why this works
Step by Step Execution
Consider this code:
async function demo() {
const numbers = [1, 2, 3];
numbers.forEach(async (n) => {
await new Promise((resolve) => setTimeout(resolve, 100));
console.log(n);
});
console.log('done');
}
demo();
What many beginners expect
They expect this output:
1
2
3
done
What actually happens
Usually this happens instead:
done
1
2
3
Step by step
demo()starts.numbersis set to[1, 2, 3].
Real World Use Cases
When to use for...of
Use sequential async loops when:
- you must process items in order
- one task depends on the previous task
- you want to avoid too many requests at once
- you are writing rate-limited API code
- you are updating records one at a time safely
Example:
for (const user of users) {
await sendWelcomeEmail(user);
}
When to use Promise.all()
Use parallel async processing when:
- tasks are independent
- speed matters
- you want to fetch multiple resources at once
- you are reading many files concurrently
Example:
const pages = await Promise.all(
urls.map((url) => fetch(url).then((res) => res.text()))
);
File processing
- Read many log files
Real Codebase Usage
In real projects, developers usually avoid forEach() for async control flow.
Common patterns
Guard clauses before async loops
async function printFiles(files) {
if (!Array.isArray(files) || files.length === 0) {
return;
}
for (const file of files) {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
}
}
This keeps the function safe and easy to read.
Early return on invalid input
async function loadUsers(ids) {
if (!ids?.length) return [];
return Promise.all(ids.map((id) => (id)));
}
Common Mistakes
1. Assuming forEach() waits
Broken example:
await files.forEach(async (file) => {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
});
Why it is wrong:
forEach()returnsundefinedawait undefineddoes nothing useful
2. Expecting the outer function to finish after the looped work
Broken example:
async function readAll(files) {
files.forEach(async (file) => {
await fs.readFile(file, 'utf8');
});
return 'finished';
}
This returns 'finished' before file reading is done.
3. Losing error handling
Comparisons
| Pattern | Waits correctly? | Runs in order? | Runs in parallel? | Good for async? |
|---|---|---|---|---|
forEach() with async callback | No | Not guaranteed in completion order | Yes, effectively starts all | No |
for...of with await | Yes | Yes | No | Yes |
map() + Promise.all() | Yes | Results keep input order | Yes | Yes |
while with |
Cheat Sheet
// Avoid this for async work
array.forEach(async (item) => {
await doSomething(item);
});
// Sequential
for (const item of array) {
await doSomething(item);
}
// Parallel
await Promise.all(array.map((item) => doSomething(item)));
Rules
forEach()does not wait for async callbacksforEach()does not return a promiseawait array.forEach(...)does not do what you want- Use
for...offor sequential async logic - Use
Promise.all()for parallel async logic - Use
Promise.allSettled()if you want all results even when some fail
FAQ
Why does async inside forEach() seem to work sometimes?
Because the callbacks still run and the awaited operations still happen. The problem is that forEach() does not wait for them, so timing and error handling may be wrong.
Can I use await with forEach() at all?
You can write it syntactically, but it does not make forEach() wait. That is why it is usually the wrong choice for async loops.
What should I use instead of forEach() for async code?
Use for...of for sequential processing or Promise.all() with map() for parallel processing.
Does Promise.all() preserve order?
Yes. The resolved results match the order of the original input array, even if promises finish at different times.
Is for...of slower than Promise.all()?
Usually yes, because it runs one task at a time. But it is the correct choice when order, safety, or rate limiting matters.
How do I handle errors for each item separately?
Mini Project
Description
Build a small file loader that reads multiple text files and prints their contents. This project demonstrates the difference between sequential async processing and parallel async processing, which is exactly the issue behind using async/await with forEach().
It is useful because reading multiple files is a real task in scripts, CLI tools, and backend applications.
Goal
Create a function that reads several files correctly using both for...of and Promise.all() patterns.
Requirements
- Create an array of file paths to read
- Implement one function that reads files sequentially
- Implement another function that reads files in parallel
- Log each file's contents to the console
- Add basic error handling for failed reads
Keep learning
Related questions
Deep Cloning Objects in JavaScript: Methods, Trade-offs, and Best Practices
Learn how to deep clone objects in JavaScript, compare structuredClone, JSON methods, and recursive approaches with examples.
Get Screen, Page, and Browser Window Size in JavaScript
Learn how to get screen size, viewport size, page size, and scroll position in JavaScript across major browsers with clear examples.
How JavaScript Closures Work: A Beginner-Friendly Guide
Learn how JavaScript closures work with simple explanations, examples, common mistakes, and practical use cases for real code.