Blog / Coding tips

JavaScript map, filter and reduce Explained

Cover image for JavaScript map, filter and reduce Explained

In JavaScript, map() transforms every item in an array and returns a new array of the same length, filter() returns a new array with only the items that pass a test, and reduce() combines all the items into a single value, like a total or an object. None of them change the original array. Below I explain each one with practical examples, show how to chain them, and cover the mistakes that cost me the most debugging time.

For a small project that uses filter() on real data, see my offline habit tracker with localStorage. Every example was run with Node.js 20 (and Node 22 for Object.groupBy), and the comments at the end of each block are the real output. The methods work the same way in every modern browser.

The sample data

Most of the examples use this small list of study tasks, saved as data.mjs, because real arrays are usually arrays of objects:

export const tasks = [
  { title: "Physics notes", subject: "Physics", minutes: 45, done: true },
  { title: "Math practice", subject: "Math", minutes: 60, done: false },
  { title: "Chemistry lab report", subject: "Chemistry", minutes: 90, done: false },
  { title: "Math revision", subject: "Math", minutes: 30, done: true },
  { title: "Physics problems", subject: "Physics", minutes: 50, done: false },
];

map(): transform every item

map() calls your function once for each item and builds a new array from whatever you return. Use it when you want "the same list, but each item changed".

import { tasks } from "./data.mjs";

const prices = [100, 250, 80];
const doubled = prices.map((price) => price * 2);
console.log(doubled);

const titles = tasks.map((task) => task.title);
console.log(titles);

// map also passes the index as the second argument
const numbered = titles.slice(0, 2).map((title, i) => `${i + 1}. ${title}`);
console.log(numbered);

console.log(prices); // the original array is unchanged

// Output:
// [ 200, 500, 160 ]
// [
//   'Physics notes',
//   'Math practice',
//   'Chemistry lab report',
//   'Math revision',
//   'Physics problems'
// ]
// [ '1. Physics notes', '2. Math practice' ]
// [ 100, 250, 80 ]

The callback gets the item first and the index second, which is handy for numbering. The input and output always have the same length. If you need fewer items, that's a job for filter().

filter(): keep only some items

filter() calls your function for each item and keeps the item if the function returns something truthy. Use it for "only the items where...".

import { tasks } from "./data.mjs";

const todo = tasks.filter((task) => !task.done);
console.log(todo.map((t) => t.title));

const longMath = tasks.filter((t) => t.subject === "Math" && t.minutes >= 45);
console.log(longMath.length, "long math task(s)");

const nothing = tasks.filter((t) => t.minutes > 500);
console.log(nothing); // no matches gives an empty array, not undefined

// Output:
// [ 'Math practice', 'Chemistry lab report', 'Physics problems' ]
// 1 long math task(s)
// []

When nothing matches, you get an empty array, not undefined or null. That means you can safely call .length or .map() on the result without checking first.

reduce(): combine everything into one value

reduce() is the one that confused me the longest. It walks through the array carrying an accumulator, the running result, and your function returns the new accumulator each time. The second argument to reduce() is the starting value.

import { tasks } from "./data.mjs";

const numbers = [5, 10, 15];
const sum = numbers.reduce((total, n) => total + n, 0);
console.log(sum);

// Total minutes across all tasks
const totalMinutes = tasks.reduce((total, task) => total + task.minutes, 0);
console.log(`${totalMinutes} minutes`);

// Count tasks per subject: the accumulator is an object
const perSubject = tasks.reduce((counts, task) => {
  counts[task.subject] = (counts[task.subject] ?? 0) + 1;
  return counts;
}, {});
console.log(perSubject);

// Output:
// 30
// 275 minutes
// { Physics: 2, Math: 2, Chemistry: 1 }

What helped me was logging each step:

[5, 10, 15].reduce((total, n) => {
  console.log(`total=${total}, n=${n} -> ${total + n}`);
  return total + n;
}, 0);

// Output:
// total=0, n=5 -> 5
// total=5, n=10 -> 15
// total=15, n=15 -> 30

The accumulator doesn't have to be a number. In the perSubject example it's an object that collects counts. The ?? 0 handles the first time a subject appears, when its count doesn't exist yet. Always return the accumulator, otherwise the next step gets undefined.

Grouping: reduce vs Object.groupBy

Grouping items by a key used to be the classic reduce() exercise. Modern JavaScript has a built-in for it:

import { tasks } from "./data.mjs";

// Object.groupBy: Node 21+, and all current major browsers
const bySubject = Object.groupBy(tasks, (task) => task.subject);
console.log(Object.keys(bySubject));
console.log(bySubject.Math.map((t) => t.title));

// Output:
// [ 'Physics', 'Math', 'Chemistry' ]
// [ 'Math practice', 'Math revision' ]

Object.groupBy is available in Node.js 21 and later and in current versions of the major browsers. It isn't in Node 20, which I confirmed by running it there, so if your code has to run on older environments, the reduce() version above still works everywhere.

Chaining map, filter and reduce

Because map() and filter() return arrays, you can chain them. Each step reads like a sentence:

import { tasks } from "./data.mjs";

// Hours of study left on unfinished tasks
const hoursLeft = tasks
  .filter((task) => !task.done)               // keep unfinished tasks
  .map((task) => task.minutes)                 // turn each into its minutes
  .reduce((total, minutes) => total + minutes, 0) / 60; // add them up

console.log(`${hoursLeft.toFixed(2)} hours left`);

// Titles of unfinished tasks, longest first, without changing the original
const plan = tasks
  .filter((t) => !t.done)
  .toSorted((a, b) => b.minutes - a.minutes)
  .map((t) => `${t.title} (${t.minutes} min)`);
console.log(plan);

// Output:
// 3.33 hours left
// [
//   'Chemistry lab report (90 min)',
//   'Math practice (60 min)',
//   'Physics problems (50 min)'
// ]

I put each method on its own line so it's easy to comment out a step while debugging. The second example uses toSorted(), which returns a sorted copy. The older sort() sorts the array in place, which can quietly change data other code depends on.

This style of small, pure functions, where the same data in always gives the same result out, is how I wrote the planner in my study app. Each task gets a score and the plan is built from the sorted list. I wrote about it in building an offline study planner with no backend. Pure functions like these are also very easy to test.

Which one should I use?

When I'm not sure, I ask what I want to get back:

  • An array of the same length, with each item changed? Use map().
  • A shorter array with some items removed? Use filter().
  • A single value, like a number, string or object? Use reduce().

Real tasks usually need more than one. Here's a small report from a list of exam results that uses all three, each for its own job:

const results = [
  { name: "Rafi", mark: 72 },
  { name: "Nusrat", mark: 88 },
  { name: "Tanvir", mark: 35 },
  { name: "Mim", mark: 64 },
];

const passed = results.filter((r) => r.mark >= 40);
const average = passed.reduce((sum, r) => sum + r.mark, 0) / passed.length;
const names = passed.map((r) => r.name).join(", ");

console.log(`${passed.length} passed: ${names}`);
console.log(`Average of passing marks: ${average.toFixed(1)}`);

// Output:
// 3 passed: Rafi, Nusrat, Mim
// Average of passing marks: 74.7

You could do all of this inside one big reduce(), but splitting it up keeps each line simple. I'd rather loop over four items three times than read one clever line that nobody understands a week later. Notice that the average divides by passed.length. If nobody passed, that would be zero and the result would be NaN, so real code should check for an empty list first.

Related methods worth knowing

Sometimes filter() is more than you need:

import { tasks } from "./data.mjs";

console.log(tasks.find((t) => t.subject === "Chemistry").title); // first match
console.log(tasks.some((t) => t.minutes > 60));   // at least one?
console.log(tasks.every((t) => t.minutes >= 30)); // all of them?
console.log(tasks.findIndex((t) => !t.done));     // index of first match

// Output:
// Chemistry lab report
// true
// true
// 1
  • find() returns the first matching item itself (or undefined), not an array.
  • some() and every() return true or false and stop as soon as they know the answer.
  • findIndex() returns the position, or -1 if nothing matches.

If you write filter(...)[0] or filter(...).length > 0, there's a clearer method for it.

Common mistakes with map, filter and reduce

1. Forgetting return in an arrow function with braces

const nums = [1, 2, 3];

const broken = nums.map((n) => { n * 2 });       // braces need a return
const fixed = nums.map((n) => { return n * 2; });
const short = nums.map((n) => n * 2);            // or drop the braces

console.log(broken, fixed, short);

// Returning an object without braces needs parentheses
const objs = nums.map((n) => ({ value: n }));
console.log(objs);

// Output:
// [ undefined, undefined, undefined ] [ 2, 4, 6 ] [ 2, 4, 6 ]
// [ { value: 1 }, { value: 2 }, { value: 3 } ]

With curly braces, the arrow function has a body and needs an explicit return. Without braces, the expression is returned automatically. To return an object literal without braces, wrap it in parentheses, or JavaScript reads the braces as a function body.

2. Passing a function that takes extra arguments

const inputs = ["10", "10", "10"];

console.log(inputs.map(parseInt));                 // index becomes the radix!
console.log(inputs.map((s) => parseInt(s, 10)));   // pass only what you mean
console.log(inputs.map(Number));

// Output:
// [ 10, NaN, 2 ]
// [ 10, 10, 10 ]
// [ 10, 10, 10 ]

map() passes the item, the index and the array. parseInt takes a second argument, the radix, so it receives the index as the base. parseInt("10", 1) is NaN and parseInt("10", 2) is 2. Wrap it in an arrow function, or use Number.

3. Calling reduce on an empty array without a starting value

const empty = [];

try {
  empty.reduce((a, b) => a + b);   // no initial value
} catch (error) {
  console.log(error.constructor.name + ":", error.message);
}

console.log(empty.reduce((a, b) => a + b, 0)); // initial value makes it safe

// Output:
// TypeError: Reduce of empty array with no initial value
// 0

Without an initial value, reduce() uses the first item as the start, and with no first item it throws. I always pass an initial value. It also makes the type of the result obvious to anyone reading.

4. Mutating objects inside map

const users = [{ name: "Rafi", active: false }];

// Wrong: this changes the original objects
const activated = users.map((u) => { u.active = true; return u; });
console.log(users[0].active); // true - the original was modified

// Right: return a new object with spread
const users2 = [{ name: "Rafi", active: false }];
const activated2 = users2.map((u) => ({ ...u, active: true }));
console.log(users2[0].active, activated2[0].active);

// Output:
// true
// false true

map() creates a new array, but the objects inside are the same objects. Changing them changes the original data. Spread into a new object instead.

5. Using map just to loop

If you don't use the returned array, use forEach() or a for...of loop. A map() whose result is thrown away tells the reader the wrong thing. And don't pass an async callback to forEach(): it won't wait, as I show in my async/await tutorial.

FAQ

What's the difference between map and forEach?

map() returns a new array built from your return values. forEach() returns undefined and is only for side effects, like logging or updating the page.

Do map, filter and reduce change the original array?

No, they return new values. But if the items are objects and your callback modifies them, those objects are changed, because both arrays point to the same objects.

Is chaining slow because it loops several times?

Each step is a separate loop, but for the arrays most apps deal with (hundreds or a few thousand items) the difference is too small to notice. Readability wins. If you ever process huge datasets, measure before optimising.

Can I use these methods on objects?

Not directly, but Object.entries(obj) turns an object into an array of [key, value] pairs you can map and filter, and Object.fromEntries() turns it back.

Conclusion

Use map() to change every item, filter() to keep some items and reduce() to boil them down to one value, and chain them to describe a data pipeline step by step. Always give reduce() a starting value, return from braced arrow functions, and avoid mutating objects inside callbacks. Python has the same idea built into the language as list comprehensions.

MDN's reference for Array.prototype.reduce() has more examples, and the pages for map and filter are linked from there.

// note

How to read this note.

This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.

If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.

Related notes

Gemini Structured Output With Zod in Next.js
Coding tips

Gemini Structured Output With Zod in Next.js

Describe the JSON once in Zod, send z.toJSONSchema() to Gemini as responseJsonSchema, then validate the reply with safeParse and retry once with the errors.

October 2, 2026 · 9 min read

How to Add a Gemini API Key to Next.js and Vercel
Coding tips

How to Add a Gemini API Key to Next.js and Vercel

Step by step: a Gemini key from Google AI Studio in .env.local, a server-only helper, a Next.js route handler, Vercel environment variables and a leak test.

October 2, 2026 · 8 min read

Build an Offline Habit Tracker With localStorage
Coding tips

Build an Offline Habit Tracker With localStorage

A small offline habit tracker in plain HTML and JavaScript: habits saved as JSON in localStorage, streaks from local dates, a 7-day row and a JSON backup.

October 2, 2026 · 10 min read