JavaScript Array Methods: map, filter, and reduce
You know how to build an array. Now comes the part where every beginner hits the same wall: you need to change the values, keep only some of them, or total…

Key topics
You know how to build an array. Now comes the part where every beginner hits the same wall: you need to change the values, keep only some of them, or total them up — and your instinct says "time for a for loop."
Here's the thing. JavaScript gives you three array methods that handle the iteration for you. Once they click, you'll stop writing a dozen lines of loop code for what should take one. The real skill isn't memorizing syntax. It's knowing what each method hands back to you before you even run it.
map changes every item. filter keeps some items. reduce collapses everything into one value. That's the whole mental model. Let's make it stick.
Why You Need These Three Methods
Imagine you have a list of prices and you need to add tax to each one. Your beginner brain reaches for a loop:
const prices = [10, 25, 50];
const withTax = [];
for (let i = 0; i < prices.length; i++) {
withTax.push(prices[i] * 1.08);
}
console.log(withTax);
[10.8, 27, 54]
That works. But it's a lot of moving parts for a simple idea: "take each price and multiply it." You're managing an index, checking a length, pushing into a new array — none of which has anything to do with the tax math you actually care about.
The array methods you'll learn here let you describe the operation while JavaScript handles the walking. Each one answers a different question:
- map — "I want a new array where every item has been transformed."
- filter — "I want a new array with only the items that pass a test."
- reduce — "I want one value that summarizes the whole array."
Before we go further, you'll want to be comfortable with two things: creating arrays and writing functions. If you've got those down, you're ready. If not, a quick refresher on either will make this tutorial smoother.
map(): Transform Every Item
map() visits every element in your array, runs a function on it, and collects whatever that function returns into a brand-new array.
Here's the smallest useful example. Double every number:
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(function (number) {
return number * 2;
});
console.log(doubled);
[2, 4, 6, 8]
You can write the same thing more compactly with an arrow function:
const numbers = [1, 2, 3, 4];
const doubled = numbers.map((number) => number * 2);
console.log(doubled);
[2, 4, 6, 8]
Both versions do the same job. The arrow function just skips some typing.
Here's what matters most: map() returns a new array and leaves the original alone.
const numbers = [1, 2, 3];
const doubled = numbers.map((number) => number * 2);
console.log(doubled);
console.log(numbers);
[2, 4, 6]
[1, 2, 3]
The original numbers array is untouched. That's not an accident — it's the whole point. You're transforming data, not destroying it.
The function you pass to map() receives the current element as its first argument. If you need the position of that element, it's available as a second argument:
const fruits = ["apple", "banana", "cherry"];
const labeled = fruits.map((fruit, index) => `${index + 1}. ${fruit}`);
console.log(labeled);
["1. apple", "2. banana", "3. cherry"]
Common mistake: forgetting to return
The most common beginner mistake with map() is forgetting the return statement inside the callback function:
const numbers = [1, 2, 3];
const doubled = numbers.map((number) => {
number * 2; // no return!
});
console.log(doubled);
[undefined, undefined, undefined]
Every function that doesn't explicitly return a value returns undefined. map() dutifully collects each undefined into the new array. The fix is simple: make sure your callback returns the transformed value.
Knowledge check
Check your understanding
Answer this question before you continue.
filter(): Keep Only What Matches
filter() runs a true/false test on every element and keeps only the ones where the test returns true. The elements themselves are not changed — they're either kept or dropped.
Say you want only the numbers greater than 10:
const scores = [15, 8, 42, 3, 27];
const highScores = scores.filter((score) => score > 10);
console.log(highScores);
[15, 42, 27]
Notice what happened: 8 and 3 failed the test, so they were left out. The values that passed came through unchanged.
Here's where beginners often get confused. map() and filter() can look similar because both take a function and both return a new array. But they answer different questions:
const numbers = [1, 2, 3, 4];
const doubled = numbers.map((number) => number * 2);
const evens = numbers.filter((number) => number % 2 === 0);
console.log(doubled);
console.log(evens);
[2, 4, 6, 8]
[2, 4]
map() changed every value. filter() kept only the values that passed the test. Same input array, completely different results.
Common mistake: returning a truthy number instead of a test result
Your callback for filter() must return a truthy or falsy value. A common beginner slip is writing something that returns the element itself:
const numbers = [1, 2, 3, 4];
const result = numbers.filter((number) => number % 2 === 0);
That's correct — the comparison number % 2 === 0 evaluates to true or false. But if you accidentally write:
const numbers = [1, 2, 3, 4];
const result = numbers.filter((number) => number % 2);
...you're returning 0 or 1, not a comparison. Since 0 is falsy and 1 is truthy, you might get the right answer by accident — until you don't. Keep your filter callbacks returning clear true/false tests.
Knowledge check
Check your understanding
Answer this question before you continue.
reduce(): Collapse Everything Into One Value
reduce() is the least intuitive of the three, so let's take it slowly.
Imagine you're walking down a line of numbers with a running total in your hand. You start at 0. You pick up the first number, add it to your total, and carry the new total forward. Then the next number. Then the next. When you reach the end, the total in your hand is the final answer.
That's reduce(). It carries a value forward — called the accumulator — and uses each element to update it.
Here's how you sum an array:
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((accumulator, currentNumber) => {
return accumulator + currentNumber;
}, 0);
console.log(sum);
10
Let's trace what happens step by step:
- The accumulator starts at
0(the initial value you passed in). - First element (
1): accumulator becomes0 + 1 = 1. - Second element (
2): accumulator becomes1 + 2 = 3. - Third element (
3): accumulator becomes3 + 3 = 6. - Fourth element (
4): accumulator becomes6 + 4 = 10.
The callback receives the accumulator first, then the current element. Whatever you return from the callback becomes the new accumulator for the next round.
That 0 at the end is the initial value, and it matters. Without it, reduce() uses the first element as the starting accumulator and begins from the second element. For a sum that still works, but for other operations it can produce surprises. Get in the habit of always providing an initial value.
The result doesn't have to be a number
Here's where reduce() gets powerful. The "one value" it produces can be a number, a string, an object, or even an array. It depends entirely on what you build in the accumulator.
const words = ["hello", "world"];
const sentence = words.reduce((accumulator, word) => {
return accumulator + " " + word;
}, "Start:");
console.log(sentence);
"Start: hello world"
Same mechanism, different result type.
Common mistake: forgetting to return the accumulator
With map() and filter(), forgetting a return gives you an array full of undefined or an empty array. With reduce(), the failure is sneakier because it builds over time.
const numbers = [1, 2, 3];
const sum = numbers.reduce((accumulator, number) => {
accumulator + number; // no return!
}, 0);
console.log(sum);
NaN
Here's what actually happens:
- The accumulator starts at
0. - First element (
1): the callback runs0 + 1but returns nothing, so the accumulator becomesundefined. - Second element (
2): the callback triesundefined + 2, which producesNaN(Not a Number). - Third element (
3):NaN + 3staysNaN.
The final result is NaN, not undefined. If you see NaN from a reduce() call, your first suspect should be a missing return inside the callback. Always return the new accumulator value.
Knowledge check
Check your understanding
Answer this question before you continue.
map, filter, and reduce Side by Side
When you're staring at a problem and wondering which method to reach for, this table is your decision rule:
| Method | What it does | What it returns | Use it when |
|---|---|---|---|
map() | Runs a function on every element | A new array with the same length | You want every item changed the same way |
filter() | Tests every element against a condition | A new array with only the items that passed | You want to keep some items and drop others |
reduce() | Carries an accumulator through every element | One single value | You want a total, summary, or combined result |
The quick version: same number of items but changed values means map(). Fewer items means filter(). One summary value means reduce().
These methods also chain together naturally. A common real-world pattern is filtering first, then mapping:
const prices = [10, 25, 50, 5, 100];
const discounted = prices
.filter((price) => price >= 20)
.map((price) => price * 0.9);
console.log(discounted);
[22.5, 45, 90]
First we kept only the prices of $20 or more. Then we applied a 10% discount to what remained. Each step passes its result to the next method, and the original array never changes.
Debugging Callback Mistakes by Reading the Output
You'll make these mistakes. Everyone does. The trick is learning to spot them from the output.
Symptom 1: map() returns an array full of undefined.
That means your callback forgot to return a value. Every element became undefined because the callback returned nothing.
Symptom 2: filter() returns an empty array.
Either no element passed your test, or your callback forgot to return a true/false comparison. Check whether you wrote a test that can actually be true for any of your data.
Symptom 3: reduce() returns NaN.
Your accumulator became invalid somewhere in the middle. The usual cause is a missing return in the callback, which turns the accumulator into undefined after the first step.
Symptom 4: The result has the wrong length or wrong values.
If you expected three items and got five, or expected numbers and got something else, check which method you used. The shape of the result tells you which method you actually called.
One more rule while you're learning: do not mutate the input array inside the callback. These methods return new arrays, but your callback can still change the original if you tell it to. Read the input, return the output, and touch nothing else. That keeps your results predictable and your debugging simple.
A debugging habit that saves time: log the result and compare its length and contents to what you expected. The output is evidence. Read it before changing your code.
Practice: Process a Real Array
Time to put all three together. Here's a starter array of student scores:
const scores = [72, 88, 55, 91, 64, 79];
Try this three-part task:
- Use
map()to add 5 bonus points to every score. - Use
filter()to keep only scores that are now 70 or above. - Use
reduce()to find the average of those passing scores.
Give it a real attempt before peeking at the solution. The goal isn't to get it right on the first try — it's to predict what each step returns before you run it.
Here's one working solution:
const scores = [72, 88, 55, 91, 64, 79];
const withBonus = scores.map((score) => score + 5);
const passing = withBonus.filter((score) => score >= 70);
const total = passing.reduce((accumulator, score) => {
return accumulator + score;
}, 0);
const average = total / passing.length;
console.log(withBonus);
console.log(passing);
console.log(average);
[77, 93, 60, 96, 69, 84]
[77, 93, 96, 84]
87.5
Notice how each step feeds the next. The map produced new values, the filter narrowed them down, and the reduce collapsed what remained into a single number you could divide.
Now try swapping the order: filter first, then map. Compare the intermediate arrays and the final average. Filtering before mapping means you only transform the scores that matter — a small difference here, but one that grows with larger data.
This same pattern shows up everywhere in browser JavaScript. Think of a product list on a shopping page: you filter to show only items in stock, then map each product object into the label text you display. Transform, select, summarize — once you can predict what each method returns, you'll start seeing opportunities to use them all over your code.
Run the practice example. Inspect the output at each step. Then try the swapped version and watch how the result changes. That curiosity is exactly what will make these methods stick.
Knowledge check
Final check
Finish the article by checking the ideas you just learned.
References
Research updated Sep 6, 2026


