DEV Community

Cover image for map vs forEach in JavaScript: The Difference Nobody Talks About (Until It Breaks You in an Interview)
Aleksandra Dudkina
Aleksandra Dudkina

Posted on • Originally published at aleksandradudkina.hashnode.dev

map vs forEach in JavaScript: The Difference Nobody Talks About (Until It Breaks You in an Interview)

There's a difference between map and forEach that most developers know but don't fully think about until it bites them in a real situation.

I want to talk about that difference and specifically about one behaviour of map that I completely forgot about when I was solving a flat polyfill in an interview. Spoiler: it cost me time and produced a wrong answer. Let's break it all down.

The Basics: What's the Key Difference?

Both map and forEach iterate over arrays. But they are fundamentally different in what they give back.

The key difference:

map() returns a new array

forEach() returns nothing (undefined)

map() - transforms data

Use map() when you want to create a new array based on the original one.

const numbers = [1, 2, 3];

const doubled = numbers.map((num) => num * 2);

console.log(doubled); // [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

map() takes each element, transforms it, and returns a new array of the same length.

forEach() - just runs code

Use forEach() when you just want to do something with each item.

const numbers = [1, 2, 3];

numbers.forEach((num) => {
  console.log(num * 2);
});
Enter fullscreen mode Exit fullscreen mode

It doesn’t return anything!

const doubled = numbers.forEach((num) => num * 2);

console.log(doubled); // undefined
Enter fullscreen mode Exit fullscreen mode

forEach runs a function on each element and returns undefined. Always. Even if you explicitly use return in your code:

const numbers = [1, 2, 3];

const doubled = numbers.forEach((num) => {
  return num * 2;
});

console.log(doubled); // undefined
Enter fullscreen mode Exit fullscreen mode

Another example:

function doubled(arr) {
  return arr.forEach((num) => {
    return num * 2;
  });
}

console.log(doubled([1, 2, 3])); 
// undefined 
Enter fullscreen mode Exit fullscreen mode

Even though we used return twice, the result is still undefined.

You would use it when you just want to do something with each element - push to another array, log something etc.

Why map() is Used in React Lists

If you’ve worked with React, you’ve definitely seen this pattern:

const listItems = users.map((user) => (
  <li key={user.id}>{user.name}</li>
));
Enter fullscreen mode Exit fullscreen mode

React needs an array of elements to render a list.

And map() returns exactly that - a new array.

Why forEach() wouldn't work in this situation:

const listItems = users.forEach((user) => (
  <li key={user.id}>{user.name}</li>
));
// nothing is returned
// nothing is rendered
Enter fullscreen mode Exit fullscreen mode

This won’t work because:

  • forEach() returns undefined

  • React receives nothing to render

Why map() Is Perfect for Chaining

One of the biggest advantages of map() is that it returns a new array, which means you can chain methods.

Chaining multiple operations:

const numbers = [1, 2, 3, 4];

const result = numbers
  .map((n) => n * 2) // doubled numbers are returned here
  .filter((n) => n > 4) // filtered numbers are returned here
  .map((n) => `Value: ${n}`); // a new map is returned here

console.log(result);
// ["Value: 6", "Value: 8"]
Enter fullscreen mode Exit fullscreen mode

Step by step:

  1. map → [2, 4, 6, 8]

  2. filter → [6, 8]

  3. map → ["Value: 6", "Value: 8"]

Each step returns a new array → enabling chaining

Why forEach() breaks chaining

const result = numbers
  .forEach((n) => n * 2) // undefined is returned
  .filter((n) => n > 4); // Error. You can't use array methods with undefined 
Enter fullscreen mode Exit fullscreen mode

This fails because:

  • forEach() returns undefined

  • You can’t call .filter() on undefined

Final recap on basics:

const nums = [1, 2, 3];

// forEach: returns undefined
const dobuledWithForEach = nums.forEach(n => n * 2);
console.log(result1); // undefined

// map: returns a new array
const dobuledWithMap = nums.map(n => n * 2);
console.log(result2); // [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode
Feature map() forEach()

Returns value

✅ New array

❌ undefined

Use case

Transform data

Side effects

Chainable

✅Yes

❌ No

When to use map():

  • You need the result, a new transformed array

  • You're rendering lists in React

  • You want to chain with .filter(), .reduce(), etc.

When to use forEach():

  • You just want to do something - log, push, trigger a side effect

  • You don't need a return value

  • You're building a result array yourself by pushing each element into it

Rule of thumb:

If you’re returning something → use map()

If you’re just doing something → use forEach()

This is the part most developers know about. But there's a less obvious difference that matters a lot in certain situations.

The Part Nobody Talks About: Empty Slots

JavaScript arrays can have empty slots. These are not the same as undefined. They are genuinely missing indices, literally creating holes in the array.

You can create them like this:

const arr = [1, , , 4]; // see, empty slots!
console.log(arr.length); // 4
console.log(arr[1]);     // undefined (but the slot is empty, not set to undefined)
Enter fullscreen mode Exit fullscreen mode

Or like this:

const arr = new Array(3); // [empty × 3]
console.log(arr.length);  // 3
Enter fullscreen mode Exit fullscreen mode

Now here's where map and forEach behave very differently:

  • map skips empty slots but keeps them in the output.

  • forEach also skips empty slots but simply doesn't call the callback for them.

const arr = [1, , , 4];

arr.forEach((item, index) => {
  console.log(index, item);
});
// 0 1
// 3 4
// (see, indices 1 and 2 are skipped entirely!)

const mapped = arr.map(item => item * 2);
console.log(mapped); // [2, empty, empty, 8]
// empty slots are kept in the output!
Enter fullscreen mode Exit fullscreen mode

See what map does? It skips the callback for empty slots but still keeps the hole in the output array. The resulting array has the same length and the same holes. You can't get rid of them with map.

It's the kind of thing that doesn't feel intuitive until you run into it.

Why This Matters: My Interview Story

I was asked to write a polyfill for Array.prototype.flat. If you haven't used it, flat takes a nested array and flattens it to a given depth:

[1, [2, [3]]].flat(1);   // [1, 2, [3]]
[1, [2, [3]]].flat(2);   // [1, 2, 3]
[1, [2, [3]]].flat(Infinity); // [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

My first attempt was using map and recursion:

function flat(arr, depth = 1) {
  return [].concat(...arr.map((item) => {
    if (depth > 0 && Array.isArray(item)) {
      return flat(item, depth - 1);
    }
    else {
      return [item];
    }
  }));
}
Enter fullscreen mode Exit fullscreen mode

Here's what happens here step by step:

  • flat is called with an array and a depth (default is 1)

  • .map() goes through each item in the array:

  • If depth is more than 0 and the item is an array, it calls flat on it with depth - 1

  • Otherwise, it puts the item in [item] to ensure map returns an array

  • [].concat(...) combines all those arrays into one flat array

The test I failed:

flat([1,2, , ,undefined,[3,4,[5,6,[7,8,[9,10]]]]], Infinity)
Enter fullscreen mode Exit fullscreen mode

Expected result: [1, 2, undefined, 3, 4, 5, 6, 7, 8, 9, 10]

Actual result: [1, 2, undefined, undefined, undefined, 3, 4, 5, 6, 7, 8, 9, 10]

See, there are 2 empty slots in this test. They are expected to be ignored but instead they are kept in the result and turned into undefined.

Remember what we said before:

const arr = [1, , , 4]; // see, empty slots!
console.log(arr.length); // 4
console.log(arr[1]);     // undefined (but the slot is empty, not set to undefined)
Enter fullscreen mode Exit fullscreen mode
  • map skips empty slots but keeps them in the output.

It keeps empty slots in the output and spread ... turns them into undefined, while it should just ignore empty slots.

Remember what we said about how forEach treats empty slots:

  • forEach skips empty slots but simply doesn't call the callback for them

Which basically means that forEach skips empty slots entirely.

Perfectly suitable for our case!

Let's rewrite our solution with forEach:

function flat(arr, depth = 1) {
  let result = [];
  arr.forEach((item) => {
    if (depth > 0 && Array.isArray(item)) {
      result.push(...flat(item, depth - 1))
    }
    else {
      result.push(item)
    }
  })
  return result
}
Enter fullscreen mode Exit fullscreen mode

Basically, almost the same solution. The difference is: as forEach doesn't return a new array, we can't use [].concat anymore (as we will be trying to concat undefined in such case), we will just push to the result array instead.

As forEach ignores empty slots entirely, it won't turn empty slots into undefined elements.

The result we get: [1, 2, undefined, 3, 4, 5, 6, 7, 8, 9, 10]

No extra undefined elements anymore.

Recap

  • map returns a new array, forEach returns undefined

  • Both skip empty slots during iteration

  • map keeps empty slots in the output

  • forEach doesn't return anything, so it just skips empty slots and moves on

Note: This is one of those things that almost never matters in day-to-day work but can come up when doing some lower-level array manipulations. Worth keeping in the back of your head.

Top comments (0)