Question

Using async/await with forEach in JavaScript

javascriptnode.jspromiseasync-awaitecmascript-2017

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.

This matters in real programming because async code often affects:

  • file processing
  • API requests
  • database queries
  • sending emails or notifications
  • batch jobs and background workers

If your loop does not actually wait when you think it does, you can get incomplete work, wrong ordering, or unhandled promise rejections.

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...of is 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.

Take Quiz

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

  • The loop pauses at each await
  • Each file is read before moving to the next one
  • printFiles() does not finish until all files are processed

Correct pattern for parallel processing

Use Promise.all() when files can be read at the same time.

import fs from 'fs/promises';

async function printFiles() {
  const files = await getFilePaths();

  const contentsArray = await Promise.all(
    files.map((file) => fs.readFile(file, 'utf8'))
  );

  contentsArray.forEach((contents) => {
    console.log(contents);
  });
}

Why this works

  • map() creates an array of promises
  • Promise.all() waits for all of them
  • Results come back in the same order as the input array

If you want parallel work with per-file logic

import fs from 'fs/promises';

async function printFiles() {
  const files = await getFilePaths();

  await Promise.all(
    files.map(async (file) => {
      const contents = await fs.readFile(file, 'utf8');
      console.log(contents);
    })
  );
}

This is valid because the promises created by map() are collected and awaited by Promise.all().

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

  1. demo() starts.
  2. numbers is set to [1, 2, 3].
  3. forEach() runs immediately over all three items.
  4. Each callback starts and returns a promise because it is async.
  5. forEach() ignores those promises.
  6. console.log('done') runs right away.
  7. After about 100ms, the async callbacks finish.
  8. Then 1, 2, and 3 are logged.

Now compare that with for...of:

async function demo() {
  const numbers = [1, 2, 3];

  for (const n of numbers) {
    await new Promise((resolve) => setTimeout(resolve, 100));
    console.log(n);
  }

  console.log('done');
}

Step by step here

  1. demo() starts.
  2. The loop begins with 1.
  3. It waits 100ms.
  4. It logs 1.
  5. It repeats for 2.
  6. It repeats for 3.
  7. Only after the loop finishes does it log done.

Output:

1
2
3
done

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
  • Load configuration fragments
  • Combine template files

API and database work

  • Fetch user profiles in parallel
  • Validate records one by one
  • Run batch imports with controlled order

The important part is choosing whether your async tasks should run one at a time or all together.

Take Quiz

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) => getUserById(id)));
}

Error handling around the whole batch

try {
  const files = await getFilePaths();
  const contents = await Promise.all(files.map((file) => fs.readFile(file, 'utf8')));
  return contents;
} catch (error) {
  console.error('Failed to load files:', error);
  throw error;
}

Per-item error handling

for (const file of files) {
  try {
    const contents = await fs.readFile(file, 'utf8');
    console.log(contents);
  } catch (error) {
    console.error(`Could not read ${file}:`, error.message);
  }
}

This is common when one failure should not stop the whole job.

map() for transformation, not side effects

In codebases, map() is often used to create values, especially promises:

const readPromises = files.map((file) => fs.readFile(file, 'utf8'));
const results = await Promise.all(readPromises);

That is clearer than using forEach() when your goal is to build async work and wait for it.

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() returns undefined
  • await undefined does 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

Broken example:

try {
  files.forEach(async (file) => {
    const contents = await fs.readFile(file, 'utf8');
    console.log(contents);
  });
} catch (error) {
  console.log('This may not catch async callback errors');
}

Use for...of or Promise.all() so errors belong to a promise you can actually await.

4. Using map() without Promise.all()

Broken example:

const results = files.map(async (file) => {
  return fs.readFile(file, 'utf8');
});

console.log(results);

results is an array of promises, not file contents.

Correct version:

const results = await Promise.all(
  files.map((file) => fs.readFile(file, 'utf8'))
);

5. Choosing parallel execution when sequential is needed

If order matters, this is risky:

await Promise.all(files.map((file) => processFile(file)));

Use for...of instead when each step must complete before the next begins.

Comparisons

PatternWaits correctly?Runs in order?Runs in parallel?Good for async?
forEach() with async callbackNoNot guaranteed in completion orderYes, effectively starts allNo
for...of with awaitYesYesNoYes
map() + Promise.all()YesResults keep input orderYesYes
while with awaitYesYesNoYes

forEach() vs for...of

  • Use forEach() for synchronous side effects
  • Use for...of when you need to await each iteration

for...of vs Promise.all()

  • Use for...of for controlled, one-at-a-time processing
  • Use Promise.all() for faster independent tasks

Promise.all() vs Promise.allSettled()

  • Promise.all() fails immediately if one promise rejects
  • Promise.allSettled() waits for every promise and reports all outcomes

Example:

const results = await Promise.allSettled(
  files.map((file) => fs.readFile(file, 'utf8'))
);
Take Quiz

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 callbacks
  • forEach() does not return a promise
  • await array.forEach(...) does not do what you want
  • Use for...of for sequential async logic
  • Use Promise.all() for parallel async logic
  • Use Promise.allSettled() if you want all results even when some fail

Quick decision guide

  • Need order? Use for...of
  • Need speed and tasks are independent? Use Promise.all()
  • Need every result including failures? Use Promise.allSettled()

Common edge case

If too many operations run in parallel, you may overload:

  • your API
  • your database
  • the file system

In that case, do not use unlimited Promise.all() without thinking about concurrency limits.

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?

Use a try/catch inside a for...of loop, or use Promise.allSettled() if running tasks in parallel.

Why does await files.forEach(...) not work?

Because forEach() returns undefined, not a promise. await only waits for promises or thenables.

Take Quiz

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
Take Quiz

Keep learning

Related questions

View more