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]
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);
});
It doesn’t return anything!
const doubled = numbers.forEach((num) => num * 2);
console.log(doubled); // undefined
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
Another example:
function doubled(arr) {
return arr.forEach((num) => {
return num * 2;
});
}
console.log(doubled([1, 2, 3]));
// undefined
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>
));
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
This won’t work because:
forEach()returnsundefinedReact 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"]
Step by step:
map→ [2, 4, 6, 8]filter→ [6, 8]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
This fails because:
forEach()returnsundefinedYou can’t call
.filter()onundefined
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]
| 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 → usemap()
If you’re just doing something → useforEach()
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)
Or like this:
const arr = new Array(3); // [empty × 3]
console.log(arr.length); // 3
Now here's where map and forEach behave very differently:
mapskips empty slots but keeps them in the output.
forEachalso 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!
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]
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];
}
}));
}
Here's what happens here step by step:
flatis 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)
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)
-
mapskips 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:
-
forEachskips 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
}
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
mapreturns a new array,forEachreturnsundefinedBoth skip empty slots during iteration
mapkeeps empty slots in the outputforEachdoesn'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)