Three array methods cover most of what you ever need to do with a list of records: filter keeps the items that match, find gives you the first one that matches, and reduce folds the whole list into a single value. Here is how each behaves on the same data, and where each one stops being the right tool.
What does filter keep?
filter runs your test against every item and returns a new array containing only the items for which the test returned a truthy value. The original array is untouched, and the new array is shorter, never longer.
const orders = [
{ id: 'A-1', status: 'paid', total: 42 },
{ id: 'A-2', status: 'pending', total: 17 },
{ id: 'A-3', status: 'paid', total: 88 },
{ id: 'A-4', status: 'refunded', total: 5 },
];
const paid = orders.filter((order) => order.status === 'paid');
console.log(paid.map((order) => order.id)); // [ 'A-1', 'A-3' ]
console.log(paid.length); // 2
Two details matter here. The callback receives one argument, the item, so the test reads like a plain question: is this order paid? And because filter returns a new array, you can chain it: orders.filter((o) => o.status === 'paid').map((o) => o.total) gives you the paid totals without touching the source list.
One more habit worth forming: filter keeps whatever your test returns as truthy, not only the value true. So items.filter(Boolean) is a tidy way to strip out null, undefined, empty strings and zeros from a list — handy after an API call that returns optional fields.
How do you get just the first matching item?
find applies the same kind of test but stops at the first hit and returns that item. If nothing matches it returns undefined, which is why you should always check before you read properties off the result.
const big = orders.find((order) => order.total > 50);
console.log(big.id); // A-3
console.log(orders.findIndex((order) => order.total > 50)); // 2
console.log(orders.find((order) => order.total > 500)); // undefined
console.log(orders.findLast((order) => order.status === 'paid').id); // A-3
findIndex gives you the position instead of the item, and findLast searches from the end — useful when the newest matching record is the one you want and your list is sorted by date. The undefined on the third line is the important one: orders.find((o) => o.total > 500).id would throw a TypeError, so write const found = orders.find(...); if (found) { ... } instead of chaining straight into a property.
How does reduce fold a list into one value?
reduce walks the array once, carrying a running value from item to item, and returns the final value. The callback takes two things: the accumulator you have built so far, and the current item. You must return the accumulator from the callback, or the next iteration receives undefined.
const basket = [
{ name: 'pen', price: 3, qty: 2 },
{ name: 'book', price: 12, qty: 1 },
];
const total = basket.reduce((sum, item) => sum + item.price * item.qty, 0);
console.log(total); // 18
Read the signature from the inside out: sum starts at 0, each order adds its line total, and the answer comes out as 18. The second argument — the 0 — is the initial value, and it decides the shape of the accumulator for the whole run.
Why should you always pass an initial value?
Without one, reduce uses the first array element as the starting accumulator. That works for addition, where the first element and the result share a type, and quietly breaks for everything else.
console.log(['a', 'b', 'a'].reduce((a, b) => a + b)); // aba
try {
[].reduce((a, b) => a + b);
} catch (error) {
console.log(error.constructor.name + ': ' + error.message);
}
// TypeError: Reduce of empty array with no initial value
The empty array case is the loud one. The quiet one is building an object: with no initial value the accumulator starts as the first item rather than an empty object, and your first assignment writes onto that item instead of onto a fresh accumulator. Passing {} or [] as the initial value removes the ambiguity and makes the callback correct for an empty input.
How do you group or count items with reduce?
Because the accumulator can be any type, reduce can build objects as well as numbers. This is how you group records by a field or count occurrences when no built-in method does exactly what you need.
const byStatus = orders.reduce((groups, order) => {
const key = order.status;
groups[key] = groups[key] ?? [];
groups[key].push(order.id);
return groups;
}, {});
console.log(byStatus);
// { paid: [ 'A-1', 'A-3' ], pending: [ 'A-2' ], refunded: [ 'A-4' ] }
const words = ['a', 'b', 'a'];
const counts = words.reduce((acc, word) => {
acc[word] = (acc[word] ?? 0) + 1;
return acc;
}, {});
console.log(counts); // { a: 2, b: 1 }
The pattern to copy is the same in both cases: read the current value for the key, fall back to an empty value with ??, write the new value back, and return the accumulator. If you find yourself writing that loop often, Object.groupBy and Map.groupBy now cover the grouping case directly — the objects module covers them in full.
What about some and every?
Two siblings complete the set. some answers “does at least one item match?” and every answers “do all items match?”. Both stop at the first decisive answer, so they are cheap on long lists.
const nums = [4, 8, 15, 16, 23, 42];
console.log(nums.some((n) => n > 40)); // true
console.log(nums.every((n) => n % 2 === 0)); // false
Which of the three should I use?
| Method | Returns | Use it when |
|---|---|---|
filter | A new array of matches | You want every matching record |
find | The first match, or undefined | You only need one record |
reduce | Whatever you define it to be | You are folding a list into one value |
some / every | A boolean | You only need a yes or no answer |
If you reach for reduce and find yourself writing a comparison inside it, filter or find was probably clearer. If you reach for filter and immediately map the result, consider whether one reduce would do the same job in a single pass.
Try it yourself: with the orders array above, write three separate helpers and print what each returns. paidOrders(orders) should give the paid records. largestOrder(orders) should give the single record with the biggest total — use reduce and make sure it returns undefined for an empty array rather than throwing. totalByStatus(orders) should reduce the list into an object where each status maps to the sum of its totals, so the result is { paid: 130, pending: 17, refunded: 5 }.