# The most common forEach mistakes in TypeScript

> forEach looks like the simplest array method there is, but in TypeScript it hides real traps: async callbacks TypeScript won't flag, narrowing lost inside closures, implicit-any indexing.

- Autore / Author: Diego Betto (https://diegobetto.com/about/)
- Pubblicato / Published: 2026-09-19
- URL: https://diegobetto.com/en/typescript-foreach-errors/
- Tag: typescript, javascript, errors

`Array.prototype.forEach` is probably the first array method most people learn, which is exactly why it keeps getting used where it isn't the right tool. In TypeScript specifically, there are at least three traps the type checker won't warn you about the way you'd expect — not because of a compiler bug, but because of how its rules are actually designed.

### 1. An `async` callback in forEach compiles fine (and that should worry you)

This is the sneakiest case because **it compiles with zero warnings**:

```typescript
async function processAll(items: string[]) {
  items.forEach(async (item) => {
    await saveToDatabase(item); // runs, but nobody's waiting for this
  });
  console.log("done"); // logged BEFORE the saves finish
}
```

`forEach`'s signature expects a callback that returns `void`. An `async` function always returns a `Promise`, so technically the return type isn't `void` — yet TypeScript doesn't complain. The reason is a specific language rule: when the expected return type is `void`, TypeScript allows the provided function to return **anything**, as long as the value is ignored. That's a deliberate design choice meant for cases like event handlers (`onClick={() => setCount(c => c + 1)}`, where the return value doesn't matter), but it has the side effect of making a real bug invisible: `forEach` doesn't wait for Promises, so the calls to `saveToDatabase` all fire off more or less at once with no control over ordering, and any rejection isn't caught by any surrounding `try/catch`.

**The fix**: if you need `await` inside the loop, `forEach` is the wrong tool. Use `for...of`:

```typescript
async function processAll(items: string[]) {
  for (const item of items) {
    await saveToDatabase(item);
  }
  console.log("done"); // now this is actually true
}
```

Or, if you want to run in parallel but still wait for completion:

```typescript
await Promise.all(items.map((item) => saveToDatabase(item)));
```

The `@typescript-eslint/no-misused-promises` rule (also available via `eslint-plugin-oxlint`, if you use [oxlint](https://diegobetto.com/en/oxlint-linter-guide/)) exists specifically to catch this pattern, because the compiler on its own won't.

### 2. `return` inside forEach is not a `break`

A common conceptual mistake for anyone coming from a classic `for` loop:

```typescript
function findFirstEven(numbers: number[]) {
  numbers.forEach((n) => {
    if (n % 2 === 0) {
      return n; // does NOT exit forEach, only the current iteration
    }
  });
}
```

TypeScript flags nothing wrong here — syntactically it's entirely valid, it's just that `return` inside the callback behaves like `continue`, not `break`. The returned value is discarded (consistent with point 1: `forEach` expects `void`), and the loop keeps going through every element even after you've already found what you were looking for.

**The fix**: if you need an early exit, `forEach` doesn't support one. You need a `for...of` loop with `break`, or `Array.prototype.find`:

```typescript
const firstEven = numbers.find((n) => n % 2 === 0);
```

### 3. Narrowing gets lost inside the callback

A less-known but real case, tied to how TypeScript reasons about function boundaries:

```typescript
function process(value: string | undefined, items: string[]) {
  if (typeof value === "undefined") return;

  items.forEach(() => {
    console.log(value.toUpperCase()); // may error here
  });
}
```

If `value` isn't `const` (or if TypeScript can't guarantee it won't be reassigned before the callback actually runs — which, with an asynchronous callback, is impossible to rule out in general), the narrowing done by the `if` above isn't guaranteed to hold inside a nested function like `forEach`'s callback. With `strict: true` — the default since TypeScript 7, covered in the [TypeScript 7](https://diegobetto.com/en/typescript-7-whats-new/) article — these cases surface far more often than they did under TypeScript 6's more permissive defaults.

**The fix**: assign the narrowed value to a new `const` variable before passing it to the callback, so the type stays pinned:

```typescript
function process(value: string | undefined, items: string[]) {
  if (typeof value === "undefined") return;

  const narrowedValue = value; // const: TypeScript can now trust it

  items.forEach(() => {
    console.log(narrowedValue.toUpperCase());
  });
}
```

### The lesson common to all three

In all three examples, TypeScript isn't flagging a syntax or type error in the strict sense — it's simply not protecting you from a _logical_ mistake the type system was never designed to catch. `forEach` is a more limited API than it looks: no early exit, no native async handling, and closures that can behave differently than expected. For most cases where reaching for it feels natural, `for...of`, `map`, `find`, or `Promise.all` are more explicit choices — and harder to misuse.
