map vs filter vs find: How to Choose the Right Array Method
The real map vs filter vs find decision is not about which array method feels more JavaScript-y. It is about the shape of the answer your program needs. map makes one new value for every old value. filter keeps the matching values. find returns the first matching value and stops. Pick by result shape first, then write the callback.
That sounds small, but it prevents a lot of code that technically works while saying the wrong thing.
The short rule
Use the method that matches the result you want:
| You need | Use | Result |
|---|---|---|
| A transformed item for every input item | map | New array, same length |
| All items that pass a test | filter | New array, same or shorter length |
| The first item that passes a test | find | One item or undefined |
The callback should match that job too:
mapgets a transform: item in, new value out.filtergets a predicate: item in, yes-or-no answer out.findgets a predicate: item in, yes-or-no answer out, but only until the first match.
If you are squinting at the callback to figure out what the method is doing, the code is already making you work too hard.
The naive approach that hides intent
A common beginner move is to reach for filter whenever there is a condition:
const users = [
{ id: 1, name: "Ada", role: "student" },
{ id: 2, name: "Grace", role: "admin" },
{ id: 3, name: "Lin", role: "student" },
];
const admin = users.filter((user) => user.role === "admin")[0];
console.log(admin);
// { id: 2, name: "Grace", role: "admin" }
The output is correct, but the code says, "build me every matching admin, then take the first one." If the rule is really "get the first admin," say that:
const admin = users.find((user) => user.role === "admin");
That version is shorter, but the important part is the promise. find tells the reader there should be one selected record, not a list.
map transforms every item
Use map when the output array should line up with the input array. Same number of items, different shape or value.
const products = [
{ sku: "A1", name: "Keyboard", price: 85 },
{ sku: "B2", name: "Mouse", price: 35 },
{ sku: "C3", name: "Monitor", price: 240 },
];
const labels = products.map((product) => `${product.sku}: ${product.name}`);
console.log(labels);
// ["A1: Keyboard", "B2: Mouse", "C3: Monitor"]
map is not asking whether an item should stay. It assumes every item produces one output item.
That is why this is suspicious:
const expensiveProducts = products.map((product) => {
if (product.price >= 100) {
return product;
}
});
console.log(expensiveProducts);
// [undefined, undefined, { sku: "C3", name: "Monitor", price: 240 }]
The callback forgot to return a value for products under 100. map still keeps the same length, so the missing branches become undefined. If you want to remove items, use filter.
filter keeps matching items
Use filter when you want a list of items that pass a test.
const expensiveProducts = products.filter((product) => product.price >= 100);
console.log(expensiveProducts);
// [{ sku: "C3", name: "Monitor", price: 240 }]
The callback should answer a boolean-style question: should this item stay?
Good filter callbacks often read like business rules:
const visibleCourses = courses.filter((course) => {
return course.published && !course.archived;
});
If your filter callback starts building strings, objects, or numbers, you are probably doing two jobs. Split them:
const visibleCourseCards = courses
.filter((course) => course.published && !course.archived)
.map((course) => ({
id: course.id,
title: course.title,
lessonCount: course.lessons.length,
}));
Now the sequence is honest: keep the visible courses, then transform each course into a card model.
find returns one matching item
Use find when the caller needs one item, usually because an id, slug, status, or priority rule points to a single useful record.
const invoice = invoices.find((invoice) => invoice.id === selectedInvoiceId);
The result is either the original matching item or undefined.
const nextBlockedStep = steps.find((step) => step.status === "blocked");
if (nextBlockedStep) {
console.log(nextBlockedStep.title);
}
That undefined result is not a failure of find. It is part of the contract. If the rest of your code wants a different fallback, make that explicit:
const overdueInvoice =
invoices.find((invoice) => invoice.status === "overdue") ?? null;
Returning null can be useful when an API or component already treats null as "nothing selected." The key is that you choose the fallback at the boundary instead of pretending a missing match cannot happen.
find can stop earlier than filter
find stops after the first match. filter has to inspect the whole array because it needs every match.
const checks = [
{ name: "syntax", failed: false },
{ name: "tests", failed: true },
{ name: "coverage", failed: true },
];
let findChecks = 0;
const firstFailure = checks.find((check) => {
findChecks += 1;
return check.failed;
});
let filterChecks = 0;
const allFailures = checks.filter((check) => {
filterChecks += 1;
return check.failed;
});
console.log(firstFailure.name); // "tests"
console.log(allFailures.length); // 2
console.log(findChecks); // 2
console.log(filterChecks); // 3
That does not mean find is always "faster" in a useful way. Most arrays in app code are small enough that readability matters more. But the early stop is a real semantic difference: find is for the first successful match, while filter is for the complete set of matches.
The callback decides the meaning
The method sets the result shape. The callback sets the rule.
const names = users.map((user) => user.name);
const admins = users.filter((user) => user.role === "admin");
const currentUser = users.find((user) => user.id === currentUserId);
Those three lines are easy to scan because each callback fits the method:
user.nametransforms a user into a string.user.role === "admin"decides whether a user stays.user.id === currentUserIddecides whether this is the selected user.
This version is harder on the reader:
const result = users
.map((user) => (user.active ? user.email : null))
.filter(Boolean)
.find((email) => email.endsWith("@company.com"));
It works, but the data shape keeps changing: users, then emails or null, then emails, then one email. If that pipeline is part of important code, name the steps:
const activeEmails = users
.filter((user) => user.active)
.map((user) => user.email);
const companyEmail = activeEmails.find((email) =>
email.endsWith("@company.com"),
);
That is a few more lines and much less detective work.
Mutating inside these methods is usually the wrong smell
map, filter, and find do not mutate the array by themselves. Your callback still can.
const updatedUsers = users.map((user) => {
user.active = false;
return user;
});
That mutates the original user objects and returns the same object references in a new array. The array is new. The objects are not.
Prefer returning new objects when you are transforming records:
const updatedUsers = users.map((user) => ({
...user,
active: false,
}));
The Spread Operator article covers the shallow-copy part of that pattern. The short version: object spread copies the top level, not every nested object.
When none of the three is right
Use some and every when the answer should be a boolean:
const hasAdmin = users.some((user) => user.role === "admin");
const allUsersHaveEmail = users.every((user) => user.email);
Use forEach vs map guidance when you are choosing between side effects and transformations. Use a for...of loop when you need break, continue, sequential await, or several pieces of changing state.
Array methods are good vocabulary. They are not a tax you pay to avoid loops.
Key Takeaways
- Choose
map,filter, orfindby result shape before writing the callback. mapreturns a new array with the same length, so conditionalmapcallbacks often create accidentalundefinedvalues.filterreturns every match. Do not usefilter(...)[0]when the rule is first match.findreturns one item orundefined, so handle the missing case deliberately.- Split pipelines when the data shape changes more than once or the method chain stops being easy to scan.