WasiLearn Code
JavaScript · Arrays and objects

map, filter and reduce

Most of what you do with an array is one of three things: transform every item, keep some of them, or boil the whole lot down to a single value. Each has a method, and using the right one makes the intention visible in the first word.

const prices = [250, 120, 80];

console.log(prices.map(p => p * 2));
console.log(prices.filter(p => p < 200));
console.log(prices.reduce((sum, p) => sum + p, 0));
output
[ 500, 240, 160 ]
[ 120, 80 ]
450

Three lines, three questions answered. Written as loops that is fifteen lines and three chances to get the counter wrong.

map transforms

map runs a function on every item and returns a new array of the results. It is always the same length as the original.

const names = ["ayesha", "bilal"];

console.log(names.map(n => n.toUpperCase()));
console.log(names.map(n => n.length));
console.log(names);
output
[ 'AYESHA', 'BILAL' ]
[ 6, 5 ]
[ 'ayesha', 'bilal' ]

The original is untouched — map copies. That is the property that makes these methods chainable and safe.

The function gets the index as a second argument when you need it:

const items = ["chai", "paratha"];

console.log(items.map((item, i) => `${i + 1}. ${item}`));
output
[ '1. chai', '2. paratha' ]

The commonest map mistake

An arrow function with braces needs an explicit return. Without one it returns undefined, and map dutifully collects those.

const prices = [250, 120];

console.log(prices.map(p => { p * 2 }));
console.log(prices.map(p => { return p * 2; }));
console.log(prices.map(p => p * 2));
output
[ undefined, undefined ]
[ 500, 240 ]
[ 500, 240 ]

p => p * 2 returns automatically because there are no braces. The moment you add braces you have written a function body, and a body returns nothing unless told to. An array full of undefined is almost always this.

filter keeps

filter runs a test on every item and returns a new array of the ones that passed. It can be shorter than the original, or empty.

const marks = [72, 45, 88, 30];

console.log(marks.filter(m => m >= 50));
console.log(marks.filter(m => m > 100));
console.log(marks.filter(m => m >= 50).length);
output
[ 72, 88 ]
[]
2

The function must return a boolean-ish value. Returning the item itself is a classic slip:

const values = [0, 1, 2, "", "chai"];

console.log(values.filter(v => v));
output
[ 1, 2, 'chai' ]

That kept every truthy value and dropped 0 and "", which is occasionally what you want and usually a bug — the same falsy trap as everywhere else.

reduce boils down

reduce walks the array carrying an accumulator, and returns whatever the accumulator ends up as.

const prices = [250, 120, 80];

console.log(prices.reduce((sum, price) => sum + price, 0));
console.log(prices.reduce((max, price) => (price > max ? price : max), 0));
output
450
250

The two arguments to reduce are the function and the starting value. That second one is easy to forget and matters:

console.log([].reduce((a, b) => a + b, 0));
output
0

Without the 0, reducing an empty array throws TypeError: Reduce of empty array with no initial value. Always supply it.

reduce is not only for numbers. Counting occurrences is the useful case:

const orders = ["chai", "coffee", "chai"];

const counts = orders.reduce((acc, item) => {
  acc[item] = (acc[item] || 0) + 1;
  return acc;
}, {});

console.log(counts);
output
{ chai: 2, coffee: 1 }

Note the return acc — the braces are back, so the return is required. This is where the earlier mistake bites hardest, because forgetting it makes the accumulator undefined on the second pass and the error appears deep inside.

If a reduce is getting hard to read, a plain loop is the better answer. It is the most powerful of the three and the easiest to overuse.

Chaining

Because map and filter return arrays, they compose:

const students = [
  { name: "Ayesha", marks: 72 },
  { name: "Bilal", marks: 45 },
  { name: "Sana", marks: 88 },
];

const passed = students
  .filter(s => s.marks >= 50)
  .map(s => s.name)
  .join(", ");

console.log(passed);
output
Ayesha, Sana

Read it top to bottom as a sentence: keep the ones who passed, take their names, join them. Filter before you map — transforming items you are about to throw away is wasted work.

Finding, testing and searching

const students = [
  { name: "Ayesha", marks: 72 },
  { name: "Bilal", marks: 45 },
];

console.log(students.find(s => s.marks < 50));
console.log(students.findIndex(s => s.marks < 50));
console.log(students.some(s => s.marks >= 70));
console.log(students.every(s => s.marks >= 70));
console.log([1, 2, 3].includes(2));
output
{ name: 'Bilal', marks: 45 }
1
true
false
true
  • find returns the first matching item, or undefined.
  • findIndex returns its position, or -1.
  • some and every return booleans and stop as soon as the answer is known.
  • includes checks for a value, not a condition.

find versus filter is worth being deliberate about: filter always walks the whole array and returns a list; find stops at the first hit and returns the item. When you want one thing, find says so.

Which methods copy and which mutate

This is the distinction that causes real bugs, because the mutating ones change your array without saying so.

Copy — map, filter, slice, concat, toSorted, toReversed. Mutate — sort, reverse, push, pop, splice.

const original = [3, 1, 2];
const sorted = original.sort();

console.log(sorted);
console.log(original);
output
[ 1, 2, 3 ]
[ 1, 2, 3 ]

sort sorted the original in place and returned the same array, so both names now point at one sorted array. If something else was relying on the original order, it has silently changed.

Copy first:

const original = [3, 1, 2];
const sorted = [...original].sort();

console.log(sorted);
console.log(original);
output
[ 1, 2, 3 ]
[ 3, 1, 2 ]

sort compares text by default

console.log([10, 9, 100].sort());
console.log([10, 9, 100].sort((a, b) => a - b));
output
[ 10, 100, 9 ]
[ 9, 10, 100 ]

With no comparison function, sort converts everything to strings and sorts alphabetically — so "100" comes before "9". It is not broken; it is documented and surprising.

Pass (a, b) => a - b for ascending numbers, b - a for descending. For strings, (a, b) => a.localeCompare(b).

forEach, and why it is last

forEach runs a function for each item and returns nothing.

["chai", "paratha"].forEach(item => console.log(item));
output
chai
paratha

It cannot be chained, and — the real limitation — it cannot be stopped. break is a syntax error inside it, and return only exits that one call.

[1, 2, 3, 4].forEach(n => {
  if (n === 3) return;
  console.log(n);
});
output
1
2
4

The return skipped 3 and carried on. If you need to stop early, use for...of with break, or find / some, which stop by design.

Reach for forEach only when you genuinely want a side effect for every item and nothing back. If you are building an array inside one, you wanted map.

Choosing

You want Method
Same length, changed items map
Fewer items, unchanged filter
One value from many reduce
The first match find
Yes or no some / every / includes
A side effect, no result forEach
To stop partway for...of with break

Next: objects, which is what the items in most real arrays turn out to be.

Prefer learning with a teacher?

WasiLearn Academy runs small-group classes covering this material, online and in Karachi.

See the classes