Real programs process collections: turn every price into a formatted string, keep only the active users, or add every item to a running total. You could write a for loop for each of these, but three higher-order methods handle most collection work more cleanly.
A higher-order function is simply a function that takes another function as an argument. map, filter, and reduce are higher-order methods on arrays, and each calls the function you give it once per element. Learning to read and write these fluently is what separates beginner JavaScript from professional JavaScript.
Why prefer them over loops? Three reasons:
- Intent is visible —
filter(isActive)tells you the goal; a loop makes you read the body to find out. - Immutability — every method returns a new array (or value), leaving the original untouched.
- Composability — you can chain them into pipelines that read left-to-right like a sentence.
flowchart LR A["input array"] --> M["map: transform"] M --> F["filter: select"] F --> R["reduce: one value"] style R fill:#e0900b,color:#fff
map: transform every element
map builds a new array by running your function on every item. The original array is untouched, and the result always has the same length as the input — no items are added or removed, only changed:
const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2);
// doubled is [2, 4, 6]; nums is still [1, 2, 3]
The callback receives three arguments: the current item, its index, and the whole array. Most of the time you only need the item, but the index is handy for numbering or looking at neighbours. Use map whenever you have an array of things and need an array of different things in the same order.
A common mistake is using map when you want a side effect. If you call map and ignore the returned array, you probably wanted forEach — or you have a design smell. map is for transformation; if you are not using the result, do not use map.
const prices = [10, 25, 7];
const withTax = prices.map((p) => p * 1.2);
const labels = prices.map((p, i) => `Item ${i + 1}: $${p}`);
console.log(withTax);
console.log(labels);
filter: keep what passes the test
filter builds a new array containing only the items where your function returns a truthy value. The result is usually shorter than the input, but it can also be empty or identical — it depends on the data:
const nums = [1, 2, 3, 4];
const big = nums.filter((n) => n > 2); // [3, 4]
const evens = nums.filter((n) => n % 2 === 0); // [2, 4]
The function you pass is called a predicate — it answers true or false for each item. A common mistake is forgetting that filter does not modify the items; it only decides which ones to keep. If you need to change them too, chain map after filter.
Another trap: returning a non-boolean. filter keeps anything truthy, so nums.filter((n) => n % 2) keeps odd numbers because 1 is truthy. Be explicit — write n % 2 === 0 rather than relying on truthiness — so the next reader knows you meant a boolean test.
flowchart LR
A["[1, 2, 3, 4]"] --> B{"n > 2 ?"}
B -->|"true"| K1["3"]
B -->|"true"| K2["4"]
B -->|"false"| D1["1"]
B -->|"false"| D2["2"]
K1 --> R["[3, 4]"]
K2 --> R
style R fill:#e0900b,color:#fff
reduce: boil down to one value
reduce is the most flexible of the three. It walks the array and folds each item into an accumulator, carrying the result forward step by step. You provide a callback (accumulator, item) and a starting value for the accumulator:
const nums = [1, 2, 3, 4];
const sum = nums.reduce((acc, n) => acc + n, 0); // 10
On the first step acc is 0 and n is 1, giving 1. On the second step acc is 1 and n is 2, giving 3, and so on. The starting value matters: pass 0 for sums, 1 for products, "" for string concatenation, or [] for building a new array.
Because reduce is so flexible, it is also the hardest to read. If you find yourself writing a multi-line reducer, ask whether map or filter would express the same idea more clearly. A pipeline of simple steps is usually better than one clever reducer.
const nums = [1, 2, 3, 4, 5, 6]; const evens = nums.filter((n) => n % 2 === 0); const sum = evens.reduce((a, b) => a + b, 0); console.log(evens); console.log(sum);
flowchart LR A["acc=0, n=1"] --> B["acc=1"] B --> C["acc=1, n=2"] C --> D["acc=3"] D --> E["acc=3, n=3"] E --> F["acc=6"] F --> G["acc=6, n=4"] G --> H["acc=10"] style H fill:#e0900b,color:#fff
Chaining into pipelines
The real power of these methods appears when you chain them. Because each returns a new array (or single value), you can attach the next method directly:
const result = orders
.filter((o) => o.active)
.map((o) => o.total)
.reduce((sum, t) => sum + t, 0);
Read this left to right: keep active orders, extract their totals, add them up. A single reduce could do the same job, but the pipeline is easier to debug — you can inspect the array after filter or after map — and easier to modify later.
One practical note: if a chain gets long, consider giving intermediate steps names. A named variable is a place to set a breakpoint, and its name documents what the step produced.
const scores = [45, 82, 67, 91, 55]; const passed = scores.filter((s) => s >= 60); const curved = passed.map((s) => Math.min(s + 5, 100)); const avg = curved.reduce((a, b) => a + b, 0) / curved.length; console.log(passed); console.log(curved); console.log(avg);
What does this print? filter keeps only the items where the test returns true, and returns them as a new array.
const nums = [1, 2, 3, 4]; console.log(nums.filter((n) => n > 2));
Keep only the numbers greater than 2.
An array is logged, so it shows in compact JSON form.
Write sumEven(nums) that returns the sum of the even numbers in nums. Chain filter (to keep evens) and reduce (to add them up).
function sumEven(nums) {
// return the sum of the even numbers
}
First filter to n % 2 === 0.
Then reduce with a starting value of 0 and add.
function sumEven(nums) {
return nums.filter((n) => n % 2 === 0).reduce((a, b) => a + b, 0);
}
Predict the output. Notice the starting value of reduce and how the accumulator builds up.
const words = ["a", "bb", "ccc"]; const result = words.reduce((acc, w) => acc + w.length, 0); console.log(result);
The accumulator starts at 0.
Add the length of each word: 1 + 2 + 3 = 6.
Write averagePositive(nums) that returns the average of all positive numbers in nums. If there are no positive numbers, return 0. Use filter and reduce.
function averagePositive(nums) {
// return the average of positive numbers
}
Filter to keep only n > 0 first.
Guard against division by zero when the filtered array is empty.
Divide the sum by positives.length.
function averagePositive(nums) {
const positives = nums.filter((n) => n > 0);
if (positives.length === 0) return 0;
const sum = positives.reduce((a, b) => a + b, 0);
return sum / positives.length;
}
Write productOfPositive(nums) that returns the product of all positive numbers in nums. If there are no positive numbers, return 1. Use filter and reduce. So productOfPositive([2, -3, 4]) returns 8.
function productOfPositive(nums) {
// return the product of positive numbers, or 1 if none
}
Filter to keep only n > 0.
Reduce with a starting value of 1 and multiply.
Starting at 1 naturally returns 1 for an empty array.
function productOfPositive(nums) {
return nums.filter((n) => n > 0).reduce((a, b) => a * b, 1);
}
Recap
maptransforms every element and preserves length — use it when you need the same number of items, changed.filterselects items with a predicate — use it when you need fewer items.reducefolds the array into a single value — use it for sums, products, or any accumulation.- Always pass a starting value to
reduce; omitting it breaks on empty arrays. - These methods chain naturally:
arr.filter(...).map(...).reduce(...). - Prefer a pipeline of simple steps over one clever reducer; readability beats brevity.
Next you'll learn how to pass functions around as values and lock private state inside closures.