You have spent half a dozen lessons brushing against this idea without giving it a name. In Function Expressions and Arrow Functions you stored functions inside a formatters object and a validators array. In Scope and Closures you wrote listMatching(filter), which took a function, and createAssigneeFilter(name), which returned one. That name is higher-order function, and it is the technique that turns repetitive code into configurable code. In this lesson you will study it properly: you will build your own versions of forEach, map, filter and reduce to understand exactly what they do inside, you will learn to compose functions into pipelines, and you will finish by putting together a reporting engine for Taller Nómada that you plug a different filter, transformer and aggregator into depending on what Marta asks for.
A note on progression: JavaScript arrays already come with
forEach,map,filterandreducebuilt in. Here you will implement them by hand to understand the mechanism; the native methods, with all their power and nuance, arrive in Iterating Over Arrays and Searching, Sorting and Aggregating Data. Until then, we carry on with loops and parallel arrays.
Contents
- What a higher-order function is
- Callbacks: the vocabulary
myForEach: doing something with each elementmyMap: transforming each elementmyFilter: keeping some of themmyReduce: boiling everything down to one value- Chaining all four over the backlog
- Functions that return functions: factories and currying
- Function composition:
composeandpipe - Functions as configuration
- Putting it all together: Taller Nómada's reporting engine
- A note on readability
- Common Mistakes and Tips
- Exercises
- Conclusion
- What a higher-order function is
The definition is short and precise:
A higher-order function (HOF) is a function that takes one or more functions as arguments, returns a function, or both.
Everything else is a normal, or first-order, function. And this is only possible because, as you saw in 03-02, functions in JavaScript are values.
// Higher-order: it TAKES a function
function repeat(times, action) {
for (let i = 0; i < times; i++) {
action(i);
}
}
repeat(3, (n) => console.log(`Reminder ${n + 1} for Iván`));
// Reminder 1 for Iván
// Reminder 2 for Iván
// Reminder 3 for Iván
// Higher-order: it RETURNS a function
function createReminder(person) {
return (subject) => `Reminder for ${person}: ${subject}`;
}
const remindLucia = createReminder('Lucía');
console.log(remindLucia('the bookings website is due on October 2'));
// Reminder for Lucía: the bookings website is due on October 2What do you gain? Separating the skeleton from the detail. repeat knows how to repeat, but it does not know what to do on each pass: you tell it that. The same skeleton works for printing, for accumulating or for validating.
flowchart LR
A["Fixed skeleton<br/>(walk, repeat, sort)"] --> C["Higher-order function"]
B["Variable detail<br/>(what to do with each element)"] --> C
C --> D["Concrete behavior"]
- Callbacks: the vocabulary
A function that is passed as an argument so another one can call it is called a callback. The associated vocabulary, worth using precisely:
| Term | Meaning | In the example |
|---|---|---|
| Higher-order function | The one that takes or returns functions | repeat |
| Callback | The function passed in to be called | (n) => console.log(...) |
| Invoking the callback | Calling it from inside the HOF | action(i) |
| Callback signature | Which parameters it will receive and what it must return | (element, index) => ... |
| Predicate | A callback that returns true/false |
(h) => h > 10 |
| Transformer | A callback that returns a new value | (h) => h * 2 |
| Comparator | A callback that returns -1, 0 or 1 |
(a, b) => a - b |
| Reducer | A callback that combines an accumulator and an element | (acc, h) => acc + h |
One point that confuses people at first: whoever defines the callback does not decide when it runs or with what arguments. The higher-order function decides that. You write the "what to do"; it supplies the "when" and the "with what".
function forEachHour(hours, callback) {
for (let i = 0; i < hours.length; i++) {
callback(hours[i], i); // ← the HOF decides: two arguments, in this order
}
}
forEachHour([12, 6, 14], (h, i) => console.log(`Task ${i + 1}: ${h} h`));
forEachHour([12, 6, 14], (h) => console.log(h)); // you can ignore the second oneThat is why, before writing a callback, you have to know what signature the function receiving it expects. It is the first thing you look for in any method's documentation.
myForEach: doing something with each element
myForEach: doing something with each elementThe simplest one: it walks the array and calls the callback with each element. It returns nothing; its worth lies in the effect it produces.
'use strict';
/**
* Calls `action` with each element of the array.
* @param {Array} list array to walk
* @param {Function} action callback (element, index, list)
*/
function myForEach(list, action) {
for (let i = 0; i < list.length; i++) {
action(list[i], i, list);
}
}
const titles = [
'Redesign the multipurpose room',
'Signage for the screen-printing workshop',
'Update the bookings website',
'Screen-printing ink inventory',
'Bookbinding guide for residents',
'Carpentry workshop quote'
];
myForEach(titles, (title, i) => {
console.log(`${i + 1}. ${title}`);
});
// 1. Redesign the multipurpose room
// 2. Signage for the screen-printing workshop
// ...The callback's three arguments (element, index, list) are not arbitrary: they are exactly the signature the native forEach uses, so learning them now saves you work in Module 4.
Compared with a classic for, what it buys you is that the loop machinery disappears: there is no i = 0, no i < list.length, no i++, and therefore no out-of-range index bugs.
One important difference from for: you cannot break out of a callback. A return inside the callback ends the callback, not the walk.
myForEach(titles, (title) => {
if (title.startsWith('Screen-printing')) return; // skips THIS one, does not stop the walk
console.log(title);
});If you need to cut out, use a for with break or a function designed for searching (you will see that in 04-05 with find and some).
myMap: transforming each element
myMap: transforming each elementmap builds a new array of the same size, where each element is the result of applying the callback to the original.
/**
* Returns a new array with the result of transforming each element.
* @param {Array} list
* @param {Function} transform callback (element, index, list) → new value
* @returns {Array} a new array of the same size
*/
function myMap(list, transform) {
const result = [];
for (let i = 0; i < list.length; i++) {
result.push(transform(list[i], i, list));
}
return result;
}
const hours = [12, 6, 14, 3, 8, 5];
const inMinutes = myMap(hours, (h) => h * 60);
console.log(inMinutes); // [ 720, 360, 840, 180, 480, 300 ]
console.log(hours); // [ 12, 6, 14, 3, 8, 5 ] ← untouched
const numbered = myMap(titles, (t, i) => `${i + 1}. ${t}`);
console.log(numbered[5]); // 6. Carpentry workshop quoteThree properties that define map and are worth committing to memory:
| Property | Consequence |
|---|---|
| It returns a new array | The original is not touched: it is a pure operation |
| Same size as the original | If 6 go in, 6 come out (for filtering there is filter) |
| One to one, in order | Element i of the result comes from element i of the original |
The most common mistake is using map when what you really want is forEach:
// ✗ a map whose result is thrown away: use forEach
myMap(titles, (t) => console.log(t)); // returns [undefined × 6]
// ✓
myForEach(titles, (t) => console.log(t));The rule: if you are going to use the resulting array, map; if you only want the effect, forEach.
myFilter: keeping some of them
myFilter: keeping some of themfilter builds a new array with the elements for which the callback (a predicate) returns a truthy value.
/**
* Returns a new array with the elements that satisfy the predicate.
* @param {Array} list
* @param {Function} predicate callback (element, index, list) → boolean
* @returns {Array} a new array, from 0 to list.length elements
*/
function myFilter(list, predicate) {
const result = [];
for (let i = 0; i < list.length; i++) {
if (predicate(list[i], i, list)) {
result.push(list[i]);
}
}
return result;
}
const longHours = myFilter(hours, (h) => h > 8);
console.log(longHours); // [ 12, 14 ]
const screenPrinting = myFilter(titles, (t) => t.toLowerCase().includes('screen-printing'));
console.log(screenPrinting);
// [ 'Signage for the screen-printing workshop', 'Screen-printing ink inventory' ]With parallel arrays, the trick is to filter indices instead of values, so you can look the other arrays up afterwards:
const assignees = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
const statuses = ['in-progress', 'pending', 'pending', 'done', 'in-progress', 'pending'];
const indices = [0, 1, 2, 3, 4, 5];
const ivansOpen = myFilter(indices, (i) => assignees[i] === 'Iván' && statuses[i] !== 'done');
console.log(ivansOpen); // [ 0, 4, 5 ]
myForEach(ivansOpen, (i) => console.log(`${titles[i]} — ${hours[i]} h`));
// Redesign the multipurpose room — 12 h
// Bookbinding guide for residents — 8 h
// Carpentry workshop quote — 5 hThis "filter the indices" pattern is a necessary workaround for as long as the data lives in parallel arrays. In Module 4, with an array of objects, it will be written directly over the tasks.
myMap |
myFilter |
|
|---|---|---|
| Size of the result | Same as the original | Between 0 and the original |
| The callback returns | A new value | A boolean |
| The elements of the result | Transformed | The originals, untouched |
| Name of the callback | Transformer | Predicate |
myReduce: boiling everything down to one value
myReduce: boiling everything down to one valuereduce is the most powerful one and the hardest to grasp. It walks the array keeping an accumulator that gets updated with each element, and at the end it returns that accumulator. The result can be a number, a string, an array or an object: anything at all.
/**
* Reduces the array to a single value.
* @param {Array} list
* @param {Function} reducer callback (accumulator, element, index, list) → new accumulator
* @param {*} initialValue the accumulator's starting value
*/
function myReduce(list, reducer, initialValue) {
let accumulator = initialValue;
for (let i = 0; i < list.length; i++) {
accumulator = reducer(accumulator, list[i], i, list);
}
return accumulator;
}The whole mechanism fits into four lines, but it is worth walking through step by step with the sum of the backlog's hours:
| Pass | acc on entry |
h |
acc + h |
|---|---|---|---|
| 0 | 0 (initial) | 12 | 12 |
| 1 | 12 | 6 | 18 |
| 2 | 18 | 14 | 32 |
| 3 | 32 | 3 | 35 |
| 4 | 35 | 8 | 43 |
| 5 | 43 | 5 | 48 |
And now the cases that show reduce is not just "adding up":
// Maximum
const maxHours = myReduce(hours, (acc, h) => (h > acc ? h : acc), hours[0]);
console.log(maxHours); // 14
// Concatenating text
const listing = myReduce(titles, (acc, t) => `${acc}\n· ${t}`, 'Backlog:');
console.log(listing);
// Backlog:
// · Redesign the multipurpose room
// · ...
// Counting by category → it returns an OBJECT
const byAssignee = myReduce(assignees, (acc, name) => {
acc[name] = (acc[name] ?? 0) + 1;
return acc;
}, {});
console.log(byAssignee); // { 'Iván': 3, Marta: 2, 'Lucía': 1 }
// Weighted effort (the rule you have been dragging along since Module 2)
const WEIGHTS = { high: 3, medium: 2, low: 1 };
const priorities = ['high', 'medium', 'high', 'low', 'medium', 'high'];
const indices2 = [0, 1, 2, 3, 4, 5];
const effort = myReduce(indices2, (acc, i) => acc + (WEIGHTS[priorities[i]] ?? 0) * hours[i], 0);
console.log(effort); // 124Two warnings about reduce:
- The reducer must always return the accumulator. Forgetting the
returnis mistake number one; the accumulator becomesundefinedand everything blows up on the next pass. - Always give it an initial value. Without one, the native version uses the first element as the accumulator and fails on empty arrays. Our
myReducewould simply returnundefined.
flowchart LR
A["initial: 0"] --> B["+12 → 12"] --> C["+6 → 18"] --> D["+14 → 32"]
D --> E["+3 → 35"] --> F["+8 → 43"] --> G["+5 → 48"]
- Chaining all four over the backlog
With the four pieces you can already express complete business questions almost declaratively. Marta's question: how many open high-priority hours are there in the backlog, and which tasks are they?
'use strict';
const TODAY = '2026-09-20';
const indices = [0, 1, 2, 3, 4, 5];
const titles = [
'Redesign the multipurpose room',
'Signage for the screen-printing workshop',
'Update the bookings website',
'Screen-printing ink inventory',
'Bookbinding guide for residents',
'Carpentry workshop quote'
];
const assignees = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
const priorities = ['high', 'medium', 'high', 'low', 'medium', 'high'];
const statuses = ['in-progress', 'pending', 'pending', 'done', 'in-progress', 'pending'];
const hours = [12, 6, 14, 3, 8, 5];
const dueDates = ['2026-09-30', '2026-10-15', '2026-10-02',
'2026-09-12', '2026-11-05', '2026-09-05'];
// 1. Filter: high priority and not finished
const openHighPriority = myFilter(indices, (i) => priorities[i] === 'high' && statuses[i] !== 'done');
console.log(openHighPriority); // [ 0, 2, 5 ]
// 2. Transform: one readable line per task
const lines = myMap(openHighPriority, (i) => {
const warning = dueDates[i] < TODAY ? ' ⚠ OVERDUE' : '';
return `${titles[i]} · ${assignees[i]} · ${hours[i]} h${warning}`;
});
// 3. Reduce: total hours
const highPriorityHours = myReduce(openHighPriority, (acc, i) => acc + hours[i], 0);
// 4. Effect: print
myForEach(lines, (line) => console.log(` ${line}`));
console.log(`Total open hours at high priority: ${highPriorityHours} h`);Output:
Redesign the multipurpose room · Iván · 12 h Update the bookings website · Lucía · 14 h Carpentry workshop quote · Iván · 5 h ⚠ OVERDUE Total open hours at high priority: 31 h
Notice the structure: filter → transform → aggregate → effect. Every step is a named operation, can be read on its own and can be changed without touching the others. Compared with the single loop from Module 2 —which did all four things jumbled together inside an if— the readability difference is enormous.
- Functions that return functions: factories and currying
You already met factories in 03-04. Here we take them a step further.
// A factory of predicates by field and value
function createPredicate(fieldArray, expectedValue) {
return (index) => fieldArray[index] === expectedValue;
}
const assignedToIvan = createPredicate(assignees, 'Iván');
const isPending = createPredicate(statuses, 'pending');
const isHigh = createPredicate(priorities, 'high');
console.log(myFilter(indices, assignedToIvan)); // [ 0, 4, 5 ]
console.log(myFilter(indices, isPending)); // [ 1, 2, 5 ]And combinators that produce new predicates out of others:
const and = (...predicates) => (i) => {
for (const p of predicates) {
if (!p(i)) return false;
}
return true;
};
const or = (...predicates) => (i) => {
for (const p of predicates) {
if (p(i)) return true;
}
return false;
};
const not = (predicate) => (i) => !predicate(i);
console.log(myFilter(indices, and(assignedToIvan, isHigh))); // [ 0, 5 ]
console.log(myFilter(indices, or(isHigh, isPending))); // [ 0, 1, 2, 5 ]
console.log(myFilter(indices, not(createPredicate(statuses, 'done')))); // [ 0, 1, 2, 4, 5 ]With three five-line functions you have built a small query language over the backlog. That is expressive power.
Currying
Currying means turning a function of several parameters into a chain of one-parameter functions:
// The normal version
function effort(weight, hours) {
return weight * hours;
}
// The "curried" version
const curriedEffort = (weight) => (hours) => weight * hours;
console.log(effort(3, 12)); // 36
console.log(curriedEffort(3)(12)); // 36
// The point is being able to fix the first argument
const highEffort = curriedEffort(3);
console.log(highEffort(12)); // 36
console.log(highEffort(14)); // 42
console.log(highEffort(5)); // 15The related technique, partial application, fixes some arguments and leaves the rest:
function withHeadroom(limit, openHours) {
return limit - openHours;
}
const weeklyHeadroom = (openHours) => withHeadroom(40, openHours); // R7 fixed
console.log(weeklyHeadroom(25)); // 15
console.log(weeklyHeadroom(45)); // -5When is it worth it? When you are going to call something many times with the same first argument, or when you need a one-parameter function to hand to myMap or myFilter. Outside those cases, currying for the sake of currying makes the code harder to read.
- Function composition:
compose and pipe
compose and pipeIf you have several small transformations, chaining them by hand is awkward:
const trimSpaces = (t) => t.trim();
const toLower = (t) => t.toLowerCase();
const stripAccents = (t) => t.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
const toHyphens = (t) => t.replace(/\s+/g, '-');
// Nested: it reads from the inside out, the opposite of the order it happens in
const slug = toHyphens(stripAccents(toLower(trimSpaces(' Carpentry Workshop Quote '))));
console.log(slug); // carpentry-workshop-quoteComposing means creating a new function that applies several in a chain:
/** compose(f, g)(x) === f(g(x)) — applied right to left */
function compose(f, g) {
return (x) => f(g(x));
}
const clean = compose(toLower, trimSpaces);
console.log(clean(' SCREEN-PRINTING LABEL ')); // screen-printing labelAnd pipe does the same thing but in reading order, left to right, which is far more natural:
/** pipe(f, g, h)(x) === h(g(f(x))) — applied left to right */
function pipe(...fns) {
return function (initialValue) {
let value = initialValue;
for (const fn of fns) {
value = fn(value);
}
return value;
};
}
const toTag = pipe(trimSpaces, toLower, stripAccents, toHyphens);
console.log(toTag(' Carpentry Workshop Quote ')); // carpentry-workshop-quote
console.log(toTag(' Nómada Café ')); // nomada-cafe
console.log(toTag('Manual Bookbinding')); // manual-bookbindingNotice that pipe is literally myReduce applied to functions instead of to data: it starts from an initial value and keeps transforming it. The direct application to the project is R9 (tags lowercased, no duplicates):
const rawTags = [' Carpentry ', 'CARPENTRY', 'Design', 'design ', 'Bookbinding'];
const normalized = myMap(rawTags, pipe(trimSpaces, toLower, stripAccents));
console.log(normalized);
// [ 'carpentry', 'carpentry', 'design', 'design', 'bookbinding' ]
const deduplicated = myReduce(normalized, (acc, t) => {
if (!acc.includes(t)) acc.push(t);
return acc;
}, []);
console.log(deduplicated); // [ 'carpentry', 'design', 'bookbinding' ]flowchart LR
A["' Carpentry Workshop Quote '"] --> B["trimSpaces"] --> C["toLower"]
C --> D["stripAccents"] --> E["toHyphens"] --> F["'carpentry-workshop-quote'"]
- Functions as configuration
The third big use of HOFs is parameterizing the behavior of a component. Instead of writing a different function for every variant, you write one and hand it the piece that changes.
// A single "sorter" that accepts any comparator
function sortIndices(indices, compare) {
const copy = [];
for (const i of indices) copy.push(i);
// Insertion sort, so we do not depend on sort() yet (it arrives in 04-05)
for (let i = 1; i < copy.length; i++) {
const current = copy[i];
let j = i - 1;
while (j >= 0 && compare(copy[j], current) > 0) {
copy[j + 1] = copy[j];
j--;
}
copy[j + 1] = current;
}
return copy;
}
const byDueDate = (a, b) => (dueDates[a] < dueDates[b] ? -1 : dueDates[a] > dueDates[b] ? 1 : 0);
const byHoursDesc = (a, b) => hours[b] - hours[a];
const byPriority = (a, b) => (WEIGHTS[priorities[b]] ?? 0) - (WEIGHTS[priorities[a]] ?? 0);
console.log(myMap(sortIndices(indices, byDueDate), (i) => `${dueDates[i]} ${titles[i]}`));
// [ '2026-09-05 Carpentry workshop quote',
// '2026-09-12 Screen-printing ink inventory',
// '2026-09-30 Redesign the multipurpose room', ... ]
console.log(myMap(sortIndices(indices, byHoursDesc), (i) => `${hours[i]} h`));
// [ '14 h', '12 h', '8 h', '6 h', '5 h', '3 h' ]The same principle applies to interchangeable validators and formatters:
function validateWith(rules, index) {
const errors = [];
myForEach(rules, (rule) => {
if (!rule.check(index)) errors.push(`${rule.code}: ${rule.message}`);
});
return errors;
}
const intakeRules = [
{ code: 'R2', message: 'The title is required.', check: (i) => titles[i].trim().length > 0 },
{ code: 'R3', message: 'The hours must be within 0-40.', check: (i) => hours[i] > 0 && hours[i] <= 40 },
{ code: 'R10', message: 'The task is overdue.', check: (i) => !(dueDates[i] < TODAY && statuses[i] !== 'done') }
];
console.log(validateWith(intakeRules, 5));
// [ 'R10: The task is overdue.' ]
console.log(validateWith(intakeRules, 0));
// []Note the pattern, because it is the same one that structures almost all professional software: a generic engine + a set of pluggable functions. Adding a rule, an ordering or a format no longer means touching the engine.
- Putting it all together: Taller Nómada's reporting engine
Let us bring everything into a single piece. generateReport knows nothing about tasks: it knows how to filter, transform, aggregate and present, and it receives each of those four things as a function.
'use strict';
/**
* A generic reporting engine.
* @param {Array} source starting elements (here, backlog indices)
* @param {Object} config
* @param {string} config.title
* @param {Function} config.filter (element) → boolean
* @param {Function} config.transform (element) → line of text
* @param {Function} config.aggregate (elements) → summary
* @param {Function} [config.sort] optional comparator
*/
function generateReport(source, config) {
const filter = config.filter ?? (() => true);
const transform = config.transform ?? ((x) => String(x));
const aggregate = config.aggregate ?? (() => '');
const sort = config.sort ?? null;
let selected = myFilter(source, filter);
if (sort !== null) selected = sortIndices(selected, sort);
const lines = myMap(selected, transform);
const output = [`── ${config.title} ──`];
myForEach(lines, (line) => output.push(` ${line}`));
output.push(` ${aggregate(selected)}`);
return myReduce(output, (acc, l) => `${acc}\n${l}`, '').trim();
}
// ─── Reusable pieces ───────────────────────────────────────────────
const notDone = (i) => statuses[i] !== 'done';
const overdue = (i) => dueDates[i] < TODAY && statuses[i] !== 'done';
const assignedTo = (name) => (i) => assignees[i] === name;
const detailedLine = (i) =>
`${titles[i]} · ${assignees[i]} · ${priorities[i]} · ${hours[i]} h · due ${dueDates[i]}`;
const compactLine = (i) => `${titles[i]} (${hours[i]} h)`;
const hoursTotal = (selection) => `Total: ${myReduce(selection, (acc, i) => acc + hours[i], 0)} h`;
const countAndWeight = (selection) => {
const n = selection.length;
const weight = myReduce(selection, (acc, i) => acc + (WEIGHTS[priorities[i]] ?? 0) * hours[i], 0);
return `${n} task(s) · weighted effort ${weight}`;
};
// ─── Report 1: everything open, by date ────────────────────────────
console.log(generateReport(indices, {
title: 'Open work by due date',
filter: notDone,
sort: byDueDate,
transform: detailedLine,
aggregate: hoursTotal
}));── Open work by due date ── Carpentry workshop quote · Iván · high · 5 h · due 2026-09-05 Redesign the multipurpose room · Iván · high · 12 h · due 2026-09-30 Update the bookings website · Lucía · high · 14 h · due 2026-10-02 Signage for the screen-printing workshop · Marta · medium · 6 h · due 2026-10-15 Bookbinding guide for residents · Iván · medium · 8 h · due 2026-11-05 Total: 45 h
// ─── Report 2: Iván's workload, compact ────────────────────────────
console.log(generateReport(indices, {
title: "Iván's open workload",
filter: and(assignedTo('Iván'), notDone),
sort: byHoursDesc,
transform: compactLine,
aggregate: countAndWeight
}));── Iván's open workload ── Redesign the multipurpose room (12 h) Bookbinding guide for residents (8 h) Carpentry workshop quote (5 h) 3 task(s) · weighted effort 67
// ─── Report 3: overdue alerts ──────────────────────────────────────
console.log(generateReport(indices, {
title: '⚠ Overdue tasks',
filter: overdue,
transform: (i) => `${titles[i]} — assignee: ${assignees[i]} — was due on ${dueDates[i]}`,
aggregate: (selection) => `${selection.length} task(s) need immediate attention`
}));── ⚠ Overdue tasks ── Carpentry workshop quote — assignee: Iván — was due on 2026-09-05 1 task(s) need immediate attention
Three completely different reports, zero changes to the engine. That is what higher-order functions buy you: when Module 6 arrives and Marta wants to filter the backlog from a dropdown in the interface, the only thing that will change is which function is passed as filter.
- A note on readability
Everything above is powerful, and precisely for that reason it is worth saying when not to use it. The functional style has a point of diminishing returns.
// ✗ Unreadable: currying + composition + a nested reduce for something trivial
const total = pipe(
(xs) => myFilter(xs, (i) => statuses[i] !== 'done'),
(xs) => myReduce(xs, (a, i) => a + hours[i], 0)
)(indices);
// ✓ Just as correct and far clearer
let total = 0;
for (let i = 0; i < hours.length; i++) {
if (statuses[i] !== 'done') total += hours[i];
}Criteria for deciding:
| Use a HOF when… | Use a normal loop when… |
|---|---|
| The operation repeats with variations | It is a one-off, simple calculation |
| The "what to do" needs to be able to change | The behavior is fixed |
| You want to name each step (filter, transform) | The loop fits in four readable lines |
| You are going to reuse the pieces | It is used only once |
| You are cutting the logic into testable pieces | You need break, continue or convoluted indices |
And a golden rule: code is written once and read fifty times. If your colleague has to draw a diagram to understand a line, that line is wrong, however elegant it may be.
Common Mistakes and Tips
1. Passing the result instead of the function.
myFilter(indices, assignedToIvan(0)); // ✗ passes `true`, not a function
myFilter(indices, assignedToIvan); // ✓The symptom: TypeError: predicate is not a function.
2. Forgetting the return in the reducer.
myReduce(assignees, (acc, n) => { acc[n] = (acc[n] ?? 0) + 1; }, {});
// ✗ TypeError: Cannot set properties of undefinedWith a block body, return acc; is mandatory.
3. Using map for its side effect. If you do not use the returned array, you wanted forEach.
4. Trying to break out of a forEach. It is not possible. Use a classic for.
5. Mutating the original array inside a callback. Modifying the list while walking it produces skips and repeated elements. Always work on the result.
6. Callbacks with hidden side effects. A predicate that also prints or modifies something makes it impossible to reason about the filter. Predicates should be pure (03-03).
7. Tip: name the callbacks that repeat. myFilter(indices, notDone) reads infinitely better than myFilter(indices, (i) => statuses[i] !== 'done') repeated seven times.
8. Tip: document the expected signature. When you write a HOF, make it clear in a comment what the callback will receive and what it must return. It is the first thing whoever uses it needs.
Exercises
Exercise 1 — mySome, myEvery and myFind
Implement three more higher-order functions, with the same callback signature (element, index, list):
mySome(list, predicate):trueif at least one satisfies it; stops as soon as it finds one.myEvery(list, predicate):trueif all satisfy it; stops as soon as one fails.myFind(list, predicate): returns the first element that satisfies it, orundefined.
Use them to answer: is there any overdue task? are they all assigned? which is the first task over 10 h?
Exercise 2 — groupBy
Write groupBy(list, getKey), a higher-order function that returns an object where each key is the result of getKey(element) and each value is an array with that group's elements. Use it to group the backlog's indices by assignee and by status, and then calculate each group's open hours.
Exercise 3 — An extended reporting engine
Extend generateReport so that it accepts two more options:
limit: the maximum number of lines to show (if it trims, add a line… and N more).headerFormat: a function(title, total) => stringthat builds the header.
Use it to generate a "Top 3 by hours" report of the open work.
Solutions
Exercise 1
function mySome(list, predicate) {
for (let i = 0; i < list.length; i++) {
if (predicate(list[i], i, list)) return true; // stops here
}
return false;
}
function myEvery(list, predicate) {
for (let i = 0; i < list.length; i++) {
if (!predicate(list[i], i, list)) return false; // stops here
}
return true;
}
function myFind(list, predicate) {
for (let i = 0; i < list.length; i++) {
if (predicate(list[i], i, list)) return list[i];
}
return undefined;
}
console.log(mySome(indices, overdue)); // true
console.log(myEvery(indices, (i) => assignees[i] !== null)); // true
console.log(myFind(indices, (i) => hours[i] > 10)); // 0
const found = myFind(indices, (i) => hours[i] > 10);
console.log(titles[found]); // Redesign the multipurpose roomComment: notice that mySome and myEvery can stop early, because the return is inside the higher-order function's loop, not inside the callback. That is the difference from myForEach. myFind returns undefined when it finds nothing, not -1 or null: that is consistent with what the native method you will see in 04-05 does.
Exercise 2
function groupBy(list, getKey) {
return myReduce(list, (groups, item, index) => {
const key = getKey(item, index, list);
if (!(key in groups)) groups[key] = [];
groups[key].push(item);
return groups; // ← essential
}, {});
}
const byAssignee = groupBy(indices, (i) => assignees[i]);
console.log(byAssignee);
// { 'Iván': [ 0, 4, 5 ], Marta: [ 1, 3 ], 'Lucía': [ 2 ] }
const byStatus = groupBy(indices, (i) => statuses[i]);
console.log(byStatus);
// { 'in-progress': [ 0, 4 ], pending: [ 1, 2, 5 ], done: [ 3 ] }
// Open hours per assignee
const names = ['Iván', 'Marta', 'Lucía'];
myForEach(names, (name) => {
const group = byAssignee[name] ?? [];
const openHours = myReduce(myFilter(group, notDone), (acc, i) => acc + hours[i], 0);
console.log(`${name}: ${openHours} h open · headroom ${40 - openHours} h`);
});
// Iván: 25 h open · headroom 15 h
// Marta: 6 h open · headroom 34 h
// Lucía: 14 h open · headroom 26 hComment: groupBy is a HOF that takes a key extractor and returns an object of groups. It is one of the most useful helper functions in existence and it appears in every utility library. The reducer's return groups is the point where most people slip up.
Exercise 3
function generateReport(source, config) {
const filter = config.filter ?? (() => true);
const transform = config.transform ?? ((x) => String(x));
const aggregate = config.aggregate ?? (() => '');
const sort = config.sort ?? null;
const limit = config.limit ?? Infinity;
const headerFormat = config.headerFormat ?? ((t, n) => `── ${t} (${n}) ──`);
let selected = myFilter(source, filter);
if (sort !== null) selected = sortIndices(selected, sort);
const total = selected.length;
const visible = myFilter(selected, (_, index) => index < limit);
const hidden = total - visible.length;
const output = [headerFormat(config.title, total)];
myForEach(myMap(visible, transform), (line) => output.push(` ${line}`));
if (hidden > 0) output.push(` … and ${hidden} more`);
output.push(` ${aggregate(selected)}`);
return myReduce(output, (acc, l) => `${acc}\n${l}`, '').trim();
}
console.log(generateReport(indices, {
title: 'Top 3 by hours',
filter: notDone,
sort: byHoursDesc,
transform: (i) => `${hours[i]} h — ${titles[i]} (${assignees[i]})`,
aggregate: hoursTotal,
limit: 3,
headerFormat: (t, n) => `▓▓ ${t.toUpperCase()} · ${n} open tasks ▓▓`
}));▓▓ TOP 3 BY HOURS · 5 open tasks ▓▓ 14 h — Update the bookings website (Lucía) 12 h — Redesign the multipurpose room (Iván) 8 h — Bookbinding guide for residents (Iván) … and 2 more Total: 45 h
Comment: aggregate still receives all the selected elements, not just the visible ones, so that the total stays the real one (45 h) rather than the total of the three lines shown. That is a design decision worth making explicit; the opposite would also be defensible, but you have to pick one and document it. Notice too the use of _ as the name of the unused parameter in myFilter(selected, (_, index) => ...): it is a widespread convention for saying "this argument exists but I am ignoring it".
Conclusion
You now have the tool that turns repetitive code into configurable code. A higher-order function takes functions, returns functions or both, and its value lies in separating the fixed skeleton —walking, filtering, sorting, reporting— from the variable detail you hand it as a callback. You have learned the vocabulary precisely: callback, predicate, transformer, comparator, reducer, and the fundamental fact that it is the higher-order function, not you, that decides when the callback is invoked and with what arguments.
You have implemented by hand the four fundamental operations over lists: myForEach (an effect on each element, no return value), myMap (a new array of the same size with each element transformed), myFilter (a new array with the ones that satisfy a predicate) and myReduce (everything reduced to one value through an accumulator). Knowing that each of them fits in four or five lines completely demystifies the native methods you will study in Iterating Over Arrays and Searching, Sorting and Aggregating Data: when you use them, you will know exactly what is happening inside.
And you have seen the three patterns built on top: factories and currying, which fix arguments and produce specialized functions (createPredicate, and, or, not, curriedEffort); composition with compose and pipe, which chains small transformations into a readable pipeline —the one that normalizes tags according to R9—; and functions as configuration, which gave rise to Taller Nómada's reporting engine: three radically different reports without touching a single line of the engine. Along with the closing warning, which matters as much as the rest: if a four-line loop is clearer, write the loop.
There is one last way of solving problems with functions, and it is the most disconcerting of all: a function that calls itself. You have brushed against it twice already —the named function expression in 03-02 existed for that, and the RangeError in 03-05 turned up because of a call that never stopped. In Recursion you will approach it methodically: the base case, the recursive case, the call stack drawn step by step, and the real problem that justifies it in Nómada Tasks: walking a task's nested subtasks to add up their hours.
JavaScript Course: From Beginner to Advanced
Module 1: Introduction to JavaScript
- What Is JavaScript?
- Setting Up Your Development Environment
- Your First JavaScript Program
- JavaScript Syntax and Basic Concepts
- Variables and Data Types
- Basic Operators
- Type Conversion and Comparisons
- The Course Project: Nómada Tasks
Module 2: Control Structures
- Conditional Statements
- Loops: for, while, do-while
- Switch Statements
- Flow Control: break, continue and Nested Loops
- Error Handling with try-catch
Module 3: Functions
- Defining and Calling Functions
- Function Expressions and Arrow Functions
- Parameters and Return Values
- Scope and Closures
- Hoisting and the Execution Context
- Higher-Order Functions
- Recursion
Module 4: Objects and Arrays
- Introduction to Objects
- Object Methods and the
thisKeyword - Arrays: Basics and Methods
- Iterating over Arrays
- Searching, Sorting and Aggregating Data: find, sort and reduce
- Array Destructuring
- Object Destructuring, Spread and Rest
- JSON and Copying Objects
Module 5: Advanced Objects and Functions
- Prototypes and Inheritance
- Classes and Object-Oriented Programming
- Encapsulation: Getters, Setters and Private Fields
- Modules: Import and Export
- Asynchronous JavaScript: Callbacks
- Promises and Async/Await
- The Event Loop and the Microtask Queue
- Iterators and Generators
Module 6: The Document Object Model (DOM)
- Introduction to the DOM
- Selecting and Manipulating DOM Elements
- Handling Events
- Propagation, Delegation and Custom Events
- Creating and Removing DOM Elements
- Rendering Lists and HTML Templates
- Handling and Validating Forms
Module 7: Browser APIs and Advanced Topics
- Local and Session Storage
- The Fetch API and AJAX
- Robust Requests: Errors, Timeouts and AbortController
- WebSockets
- Service Workers and Progressive Web Apps (PWAs)
- Essential Browser APIs
- Introduction to WebAssembly
Module 8: Testing and Debugging
- Debugging JavaScript
- Code Quality: ESLint, Prettier and Conventions
- Unit Testing with Jest
- Test Doubles: Mocks, Stubs and Spies
- Integration Testing
- End-to-End Testing with Cypress
Module 9: Performance and Optimization
- Measure Before You Optimize: DevTools and Web Vitals
- Optimizing JavaScript Performance
- Memory Management
- Efficient DOM Manipulation
- Lazy Loading and Code Splitting
Module 10: JavaScript Frameworks and Libraries
- Why Frameworks Exist
- Introduction to React
- State Management with Redux
- Vue.js Basics
- Angular Basics
- Choosing the Right Framework
