Both map() and forEach() loop over every element in an array and call a callback. That's where the similarity ends. They have different return values, different intentions, and using one where you meant the other produces either wrong results or confusing code.
The One-Line Difference
forEach — do something with each element. Returns undefined. For side effects only.
map — transform each element into something new. Returns a new array. For data transformation.
const numbers = [1, 2, 3, 4, 5]
// forEach: iterate and do something — returns undefined numbers.forEach(n => console.log(n * 2)) // logs 2, 4, 6, 8, 10 const result1 = numbers.forEach(n => n * 2) result1 // undefined — forEach always returns undefined
// map: transform and collect — returns a new array const doubled = numbers.map(n => n * 2) doubled // [2, 4, 6, 8, 10] — new array, original untouched numbers // [1, 2, 3, 4, 5] — unchanged
Why Return Value Matters
The return value is the entire design difference. map collects the return value of each callback into a new array. forEach discards the return value of each callback — whatever you return from a forEach callback is thrown away.
// ❌ Common mistake: using forEach expecting a result
const names = ['alice', 'bob', 'charlie']
const capitalized = names.forEach(name => name.charAt(0).toUpperCase() + name.slice(1) )
capitalized // undefined — forEach never returns the transformed values
// ✓ Correct: use map when you need the transformed array
const capitalized = names.map(name =>
name.charAt(0).toUpperCase() + name.slice(1)
)
capitalized // ['Alice', 'Bob', 'Charlie']
This is the #1 forEach bug in real codebases: using it when you need map, wondering why the result is always undefined.
When to Use forEach
forEach is correct when the loop's purpose is side effects — when you want to do something with each element rather than transform it into a new value.
const products = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Phone', price: 599 },
]
// Side effects — updating external state, not producing a new array products.forEach(product => { analytics.track('product_viewed', { id: product.id }) })
// Side effects — writing to DOM products.forEach(product => { const li = document.createElement('li') li.textContent = product.name list.appendChild(li) })
// Side effects — logging products.forEach((product, index) => { console.log(${index + 1}. ${product.name}: $${product.price}) })
In all these cases, you don't need a returned array. You're performing an action. forEach is semantically correct.
When to Use map
map is correct when you're transforming data — when you want a new array where each element corresponds to a transformed version of the original.
const users = [
{ id: 1, firstName: 'Alice', lastName: 'Smith' },
{ id: 2, firstName: 'Bob', lastName: 'Jones' },
]
// Transform: create a new array of full names const fullNames = users.map(u => ${u.firstName} ${u.lastName}) // ['Alice Smith', 'Bob Jones']
// Transform: extract specific fields const ids = users.map(u => u.id) // [1, 2]
// Transform: reshape objects const displayUsers = users.map(u => ({ id: u.id, name: ${u.firstName} ${u.lastName}, })) // [{ id: 1, name: 'Alice Smith' }, { id: 2, name: 'Bob Jones' }]
// Transform: chain with filter const expensiveNames = products .filter(p => p.price > 800) .map(p => p.name) // ['Laptop']
The Chainability Difference
map returns an array, which means you can chain it directly with other array methods. forEach returns undefined, so chaining breaks:
const numbers = [1, 2, 3, 4, 5, 6]
// ✓ map chains naturally const result = numbers .filter(n => n % 2 === 0) // [2, 4, 6] .map(n => n * n) // [4, 16, 36] .reduce((sum, n) => sum + n, 0) // 56
// ❌ forEach breaks chains — nothing to chain onto const broken = numbers .filter(n => n % 2 === 0) .forEach(n => n * n) // returns undefined .reduce(...) // TypeError: Cannot read properties of undefined
The moment you want to chain array methods, map is the right tool.
Mutation Warning
Neither map nor forEach should mutate the original array. map is designed for immutable transformation — it produces a new array. forEach doesn't return anything, so mutating inside it is the only way to "produce a result," which is an antipattern.
const items = [{ val: 1 }, { val: 2 }, { val: 3 }]
// ❌ Antipattern: mutating objects inside map const mutated = items.map(item => { item.val *= 2 // mutates the original object return item }) // items is now also changed — this defeats the purpose of map
// ✓ Correct: map produces genuinely new objects const transformed = items.map(item => ({ ...item, val: item.val * 2 })) // items is untouched, transformed is a new array of new objects
Performance: Is There a Difference?
Practically, no. Both iterate every element once — O(n). The overhead difference is immeasurable for typical array sizes. Don't choose between them based on performance.
Choose based on intention:
- "I want a new array of transformed values" →
map
- "I want to perform an action per element" →
forEach
If performance actually matters (millions of items), benchmark with your actual data. At that scale, a plain for loop is faster than both because it has no callback overhead.
Quick Decision Guide
Need a new array of transformed values? → map()
Need to chain with filter/reduce/etc? → map()
Performing side effects (DOM, logging, API)? → forEach()
Don't need a return value? → forEach()
The Real Cost of Getting This Wrong
// This bug exists in production codebases everywhere:
function getDisplayNames(users) {
return users.forEach(u => u.displayName) // always returns undefined
// ↑ should be .map()
}
const names = getDisplayNames(users) names.length // TypeError: Cannot read properties of undefined (reading 'length')
The error appears far from the cause — a null check fails, a component crashes on render, an API returns an unexpected shape — and the root is forEach where map was needed.
Use map when you care about what the callback returns. Use forEach when you don't.