JavaScript Array Methods
JavaScript array methods are not a prize for avoiding loops. They are a vocabulary for saying what kind of result you want from an array. Use map when every item becomes another item. Use filter when some items survive. Use reduce when the whole array becomes one value. Reach for the wrong one and the code still runs, which is exactly why the bugs can feel so slippery.
If you want the short rule: choose the method by the shape of the output, not by which method looks clever.
The naive approach
Beginners often learn forEach first, then use it for everything:
const prices = [10, 20, 30];
const discounted = [];
prices.forEach((price) => {
discounted.push(price * 0.9);
});
This works, but the intent is hidden. The code is really saying, "make a new array where every price is discounted." That is exactly what map already means:
const prices = [10, 20, 30];
const discounted = prices.map((price) => price * 0.9);
The result is the same. The second version gives the reader a cleaner promise: the output has one transformed item for every input item.
Choose by output shape
Most array-method decisions can be made from this table:
| Need | Method | Output shape |
|---|---|---|
| Transform every item | map | New array, same length |
| Keep matching items | filter | New array, same or shorter length |
| Find one item | find | One value or undefined |
| Check whether any item passes | some | Boolean |
| Check whether all items pass | every | Boolean |
| Collapse everything into one result | reduce | Any single value |
| Do an action for each item | forEach | undefined |
That output shape is the contract. If you use map and ignore the returned array, you probably wanted forEach. If you use forEach and push into another array, you probably wanted map or filter. The forEach vs map guide goes deeper on that specific split.
map transforms every element
map runs a callback for each item and puts each return value into a new array.
const users = [
{ id: 1, name: "Ada" },
{ id: 2, name: "Grace" },
{ id: 3, name: "Katherine" },
];
const labels = users.map((user) => `${user.id}: ${user.name}`);
console.log(labels);
// ["1: Ada", "2: Grace", "3: Katherine"]
The original array is untouched. The objects inside it may still be shared references, so do not mutate them inside map unless you genuinely intend to mutate the original data.
const updated = users.map((user) => ({
...user,
selected: false,
}));
That object spread creates a new object for each user. For the shallow-copy caveat, see Spread Operator.
filter keeps matching elements
filter expects the callback to return a truthy or falsy value. Items with a truthy result stay in the new array.
const tasks = [
{ title: "Ship lesson", done: true },
{ title: "Fix typo", done: false },
{ title: "Record walkthrough", done: true },
];
const completed = tasks.filter((task) => task.done);
console.log(completed);
// [{ title: "Ship lesson", done: true }, { title: "Record walkthrough", done: true }]
The callback should be a predicate: one item in, yes-or-no answer out. If your callback returns transformed data, you are probably mixing filter and map.
const namesOfCompletedTasks = tasks
.filter((task) => task.done)
.map((task) => task.title);
That chain reads in order: keep completed tasks, then extract their titles.
reduce collapses the array
reduce takes an accumulator and updates it for each item. It can produce a number, string, object, array, Map, or whatever else your code needs.
const cart = [
{ name: "Course", price: 39 },
{ name: "Workbook", price: 12 },
{ name: "Exam pack", price: 19 },
];
const total = cart.reduce((sum, item) => sum + item.price, 0);
console.log(total); // 70
Always give reduce an initial value. Without it, JavaScript uses the first array item as the accumulator, which is a lovely way to get a bug shaped like a question mark.
const empty = [];
empty.reduce((sum, n) => sum + n);
// TypeError: Reduce of empty array with no initial value
empty.reduce((sum, n) => sum + n, 0);
// 0
Use reduce when the output really is one accumulated result:
const votes = ["yes", "no", "yes", "yes", "no"];
const counts = votes.reduce((result, vote) => {
result[vote] = (result[vote] ?? 0) + 1;
return result;
}, {});
console.log(counts);
// { yes: 3, no: 2 }
If a reduce takes more than a few seconds to understand, split it into named steps. Clever reductions age badly.
find, some, and every avoid noisy filters
You do not need filter when you only care about one item.
const users = [
{ id: 1, role: "student" },
{ id: 2, role: "admin" },
];
const admin = users.find((user) => user.role === "admin");
find stops when it finds the first match. filter always keeps scanning because it has to build the full list of matches.
Use some for "does at least one item pass?"
const hasAdmin = users.some((user) => user.role === "admin");
Use every for "do all items pass?"
const allHaveIds = users.every((user) => typeof user.id === "number");
These methods communicate intent and can stop early. That is useful for both readability and work avoided.
Chaining is allowed, but it is not a personality
Chains are best when each step has a clean job:
const totalForActiveUsers = users
.filter((user) => user.active)
.map((user) => user.balance)
.reduce((sum, balance) => sum + balance, 0);
That is readable because each method matches the output shape: keep active users, extract balances, sum balances.
This is less helpful:
const result = users
.map((user) => ({ ...user, label: user.name.trim().toLowerCase() }))
.filter((user) => user.label)
.map((user) => user.label.replace(/\s+/g, "-"))
.filter((label) => label.length > 2)
.reduce((acc, label) => `${acc},${label}`, "");
At that point, name the intermediate steps or write a small helper. Dense code is not advanced code. It is just expensive to read.
When a loop is better
Array methods are not always the clearest answer. Use a for...of loop when you need break, continue, sequential await, or several pieces of state changing together.
for (const user of users) {
if (user.disabled) continue;
if (user.role === "admin") {
sendAdminAlert(user);
break;
}
}
The for...of loop exists for this kind of control flow. Do not fight the language to make everything a chain.
Key Takeaways
- Pick the method by output shape: transformed array, filtered array, one value, boolean, or side effect.
map,filter, andreducereturn new values, but they do not deep-copy nested objects for you.- Give
reducean initial value unless you enjoy empty-array surprises. find,some, andeveryare often clearer than filtering and then checking the result.- A plain
for...ofloop is the right tool when control flow matters more than pipeline style.