Skip to main content
Being Idea Innovations
map, filter and reduce in JavaScript: A Practical Guide
Back to Blog

map, filter and reduce in JavaScript: A Practical Guide

24 April 20236 min readJavascript
Share:

map(), filter() and reduce() are the three array methods that make JavaScript feel functional. Each takes a callback, each returns something new, and none of them modifies the original array.

The textbook examples — doubling numbers, filtering evens — show the syntax but not why these methods matter. This guide uses realistic data and covers the parts that actually trip people up.

The working data

const orders = [
  { id: 1, customer: 'Ada',   total: 120.00, status: 'paid',    items: 3 },
  { id: 2, customer: 'Grace', total:  45.50, status: 'pending', items: 1 },
  { id: 3, customer: 'Ada',   total: 220.75, status: 'paid',    items: 5 },
  { id: 4, customer: 'Alan',  total:  15.00, status: 'refunded', items: 1 },
  { id: 5, customer: 'Grace', total: 310.20, status: 'paid',    items: 8 },
];

map — transform every element

map() returns a new array of the same length, with each element replaced by whatever the callback returns.

const summaries = orders.map(order => `#${order.id} — ${order.customer}: £${order.total.toFixed(2)}`);
// ["#1 — Ada: £120.00", "#2 — Grace: £45.50", …]

// Reshaping objects — the most common real use
const rows = orders.map(({ id, customer, total }) => ({ id, customer, total }));

Note the parentheses around ({ id, customer, total }). Without them, JavaScript reads the braces as a function body rather than an object literal and you get undefined for every element. This catches everyone at least once.

Do not use map for side effects

// Wrong — builds an array of undefined and throws it away
orders.map(order => console.log(order.id));

// Right
orders.forEach(order => console.log(order.id));

If you are not using the returned array, you wanted forEach. Using map allocates a whole array for nothing and misleads the next reader into looking for a result that does not exist.

filter — keep the elements you want

filter() returns a new array containing only elements for which the callback returned something truthy. Length is zero to original length.

const paid = orders.filter(order => order.status === 'paid');        // 3 orders
const large = orders.filter(order => order.total > 100);             // 3 orders
const adasPaid = orders.filter(o => o.customer === 'Ada' && o.status === 'paid');

A useful idiom — Boolean as the callback removes every falsy value:

const values = ['a', '', null, 'b', undefined, 0, 'c'];
const clean = values.filter(Boolean);   // ["a", "b", "c"]

Careful with that one: it also strips 0 and '', which may be legitimate data. For arrays of numbers use an explicit predicate.

filter always returns an array

// Looking for one thing? filter gives you an array you then have to unwrap.
const order = orders.filter(o => o.id === 3)[0];   // works, but wasteful

// find returns the element itself, and stops at the first match.
const order = orders.find(o => o.id === 3);

find() short-circuits; filter() always walks the entire array. On large collections that difference is real.

reduce — collapse to a single value

reduce() is the general-purpose one. It walks the array carrying an accumulator, and returns whatever the accumulator holds at the end. That result can be a number, a string, an object or another array.

const revenue = orders
  .filter(o => o.status === 'paid')
  .reduce((sum, order) => sum + order.total, 0);
// 650.95

The second argument to reduce0 here — is the initial accumulator value.

Always pass the initial value

This is the reduce bug that reaches production:

[].reduce((a, b) => a + b);       // TypeError: Reduce of empty array with no initial value
[].reduce((a, b) => a + b, 0);    // 0 — correct

Without an initial value, reduce uses the first element as the accumulator and starts from the second. On an empty array there is no first element, so it throws. Since arrays that are usually populated are occasionally empty, omitting the initial value means a crash that only appears in the edge case.

It also changes the types involved. Building an object without an initial {} makes the first element your accumulator, which is almost never what you meant.

reduce is not just for sums

Grouping is where reduce earns its place:

const byCustomer = orders.reduce((groups, order) => {
  (groups[order.customer] ??= []).push(order);
  return groups;
}, {});

// { Ada: [order1, order3], Grace: [order2, order5], Alan: [order4] }

Counting:

const statusCounts = orders.reduce((counts, order) => {
  counts[order.status] = (counts[order.status] ?? 0) + 1;
  return counts;
}, {});
// { paid: 3, pending: 1, refunded: 1 }

Building a lookup index, which turns repeated find() calls into constant-time access:

const byId = orders.reduce((index, order) => {
  index[order.id] = order;
  return index;
}, {});

byId[3].customer;   // "Ada" — no scanning

Computing several things in one pass:

const stats = orders.reduce((acc, order) => ({
  count: acc.count + 1,
  total: acc.total + order.total,
  items: acc.items + order.items,
  max:   Math.max(acc.max, order.total),
}), { count: 0, total: 0, items: 0, max: -Infinity });

Object.groupBy

Grouping is common enough that JavaScript now has it built in:

const byStatus = Object.groupBy(orders, order => order.status);
// { paid: [...], pending: [...], refunded: [...] }

Available in current browsers and Node 21+. Clearer than the reduce equivalent when you can rely on it.

Chaining

Because each method returns a new array, they compose:

const topPaidCustomers = orders
  .filter(order => order.status === 'paid')
  .map(order => ({ customer: order.customer, total: order.total }))
  .sort((a, b) => b.total - a.total)
  .slice(0, 3);

Filter before you map. Mapping first transforms elements you are about to discard.

One warning: sort() mutates the array it is called on. In the chain above that is safe because map() already produced a fresh array, but calling orders.sort(…) directly would reorder your original data. Use toSorted() where available, or copy first with [...orders].sort(…).

The other arguments

All three callbacks receive (element, index, array):

const numbered = orders.map((order, index) => `${index + 1}. ${order.customer}`);

This causes a classic bug when combined with functions that take their own second parameter:

['1', '2', '3'].map(parseInt);        // [1, NaN, NaN]  — not what you wanted
['1', '2', '3'].map(Number);          // [1, 2, 3]
['1', '2', '3'].map(s => parseInt(s, 10));   // [1, 2, 3]

map passes the index as parseInt's second argument, the radix — so it parses "2" in base 1 and "3" in base 2, both invalid.

flatMap

Map and flatten one level in a single pass:

const allItems = orders.flatMap(order => order.lineItems);

// Also useful for filter-and-map together: return [] to drop an element.
const paidTotals = orders.flatMap(o => o.status === 'paid' ? [o.total] : []);

When not to use them

These methods always process every element. A plain loop can stop early:

// Walks all million elements
const found = hugeArray.filter(x => x.id === target)[0];

// Stops at the match
const found = hugeArray.find(x => x.id === target);

// Need to break on a condition mid-iteration? Use a loop.
for (const item of hugeArray) {
  if (item.corrupt) break;
  process(item);
}

Also prefer a loop when the operation is genuinely imperative — writing to a database, awaiting each result in sequence. await inside map does not do what people expect; it returns an array of promises, which you then have to hand to Promise.all().

// Runs in parallel — usually what you want
const results = await Promise.all(orders.map(o => fetchDetails(o.id)));

// Runs one at a time — when you need sequencing or rate limiting
for (const order of orders) {
  await fetchDetails(order.id);
}

Quick reference

MethodReturnsLengthStops early
mapNew arraySameNo
filterNew array0 to sameNo
reduceAny valueNo
findElement or undefinedYes
some / everyBooleanYes
forEachundefinedNo

None of these mutate the array they are called on — unlike sort, reverse, splice and push, which do.

Share:

Written by

Amit Verma

Founder / Senior Software Engineer

Amit leads engineering at Being Idea. With 15+ years building scalable software products across global markets, he drives architecture decisions and engineering culture across every engagement.

More articles by Amit

Want to talk tech?

We ship software that scales. Let's work together.

No long-term contracts
Senior engineers only
US · AU · NZ timezone coverage
14-day trial on retainers