Closing the previous lesson left a very concrete to-do list: finding the first task that meets a condition cost you an entire function, grouping by status cost you a loop inside another one, the summary cost you three accumulator variables, and sorting the board by due date you did not even know how to do. All those operations are so universal that JavaScript ships them ready-made and with names of their own. In this lesson you will learn the full arsenal for querying a list: find, findIndex, findLast, some, every and filter for searching and selecting; sort for ordering —with the language's most famous trap, the one that makes [10, 9, 100] sort wrongly—; and reduce, the most powerful and worst-explained tool of them all, which we will take slowly and through four real cases. At the end you will assemble the Taller Nómada weekly workload report, the deliverable Marta has been asking for since Module 1.
Contents
- The starting point: the canonical backlog
- Finding one element:
find,findIndex,findLast - Checking the whole set:
someandevery - Selecting several:
filter - The searching table
sort: the numbers trap- The comparator function
- Sorting by number, by text and by date
- Descending order and sorting by several criteria
toSortedandtoReversed: sorting without mutatingreduce, slowly- Four real cases of
reduce Object.groupBy,MapandSet- Chaining
filter().map().sort()and its cost - Worked example: the weekly workload report
- Common Mistakes and Tips
- Exercises
- Conclusion
- The starting point: the canonical backlog
Everything that follows works on the same backlog from 04-01. We repeat it in compact form to keep it at hand:
'use strict';
const TODAY = '2026-09-20';
const WEIGHTS = { high: 3, medium: 2, low: 1 };
const backlog = [
{ id: 1, title: 'Redesign the multipurpose room', assignee: 'Iván', priority: 'high', status: 'in-progress', tags: ['space', 'design'], estimatedHours: 12, dueDate: '2026-09-30', reviewer: 'Marta' },
{ id: 2, title: 'Signage for the screen-printing workshop', assignee: 'Marta', priority: 'medium', status: 'pending', tags: ['screen-printing', 'communication'], estimatedHours: 6, dueDate: '2026-10-15', reviewer: null },
{ id: 3, title: 'Update the bookings website', assignee: 'Lucía', priority: 'high', status: 'pending', tags: ['web', 'bookings'], estimatedHours: 14, dueDate: '2026-10-02', reviewer: 'Iván' },
{ id: 4, title: 'Screen-printing ink inventory', assignee: 'Marta', priority: 'low', status: 'done', tags: ['screen-printing', 'storeroom'], estimatedHours: 3, dueDate: '2026-09-12', reviewer: null },
{ id: 5, title: 'Bookbinding guide for residents', assignee: 'Iván', priority: 'medium', status: 'in-progress', tags: ['bookbinding', 'documentation'], estimatedHours: 8, dueDate: '2026-11-05', reviewer: 'Lucía' },
{ id: 6, title: 'Carpentry workshop quote', assignee: 'Iván', priority: 'high', status: 'pending', tags: ['carpentry', 'purchasing'], estimatedHours: 5, dueDate: '2026-09-05', reviewer: 'Marta' }
];Every method in this lesson takes a function as an argument: they are the higher-order functions you studied in 03-06. The function that returns true or false to decide whether an element "counts" is called a predicate.
- Finding one element:
find, findIndex, findLast
find, findIndex, findLastfind(predicate) returns the first element that meets the condition, or undefined if there is none. And it stops as soon as it finds it.
const overloaded = backlog.find((t) => t.estimatedHours > 10);
console.log(overloaded.title); // 'Redesign the multipurpose room'
const missing = backlog.find((t) => t.estimatedHours > 100);
console.log(missing); // undefinedCompare it with the function you wrote by hand in exercise 2 of the previous lesson:
// Before: seven lines
function firstOverloadedTask(tasks, limit) {
for (const task of tasks) {
if (task.estimatedHours > limit) return task;
}
return null;
}
// Now: one
const first = backlog.find((t) => t.estimatedHours > 10);The only behavioral difference is that find returns undefined where your function returned null. Since the result may not exist, always protect yourself with ?. or ??:
findIndex(predicate) returns the position instead of the element, or -1:
console.log(backlog.findIndex((t) => t.id === 6)); // 5
console.log(backlog.findIndex((t) => t.id === 99)); // -1It is what you need when you are going to modify the array by position, for example with splice:
function removeById(tasks, id) {
const position = tasks.findIndex((t) => t.id === id);
if (position === -1) return null;
const [removed] = tasks.splice(position, 1);
return removed;
}findLast and findLastIndex do the same thing walking from the end:
console.log(backlog.find((t) => t.priority === 'high').title);
// 'Redesign the multipurpose room' ← the first high-priority one
console.log(backlog.findLast((t) => t.priority === 'high').title);
// 'Carpentry workshop quote' ← the last high-priority oneRemember, too, the limitation of includes and indexOf you saw in 04-03: they compare with === and are therefore no use with objects. find is, because you write the comparison yourself:
console.log(backlog.includes({ id: 6 })); // false ✗
console.log(backlog.find((t) => t.id === 6) !== undefined); // true ✓
- Checking the whole set:
some and every
some and everySometimes you do not want the element, but a yes-or-no answer about the whole list.
// some: is there AT LEAST ONE that matches?
const hasOverdue = backlog.some((t) => t.dueDate < TODAY && t.status !== 'done');
console.log(hasOverdue); // true (number 6, "Carpentry workshop quote")
// every: do they ALL match?
const allHaveAssignee = backlog.every((t) => typeof t.assignee === 'string');
console.log(allHaveAssignee); // true
const allReviewed = backlog.every((t) => t.reviewer !== null);
console.log(allReviewed); // false (tasks 2 and 4 have no reviewer)Both short-circuit, just like the && and || operators from 01-06: some stops at the first true, every stops at the first false. That makes them the correct —and fastest— way to validate.
Two behaviors with an empty array that are surprising and worth knowing:
console.log([].some((t) => true)); // false ← there is none that matches
console.log([].every((t) => false)); // true ← "all" zero elements matchThat true is called vacuous truth and is mathematically correct ("every unicorn on this list is blue" is true if the list is empty). In code, keep it in mind: every over an empty list never flags anything.
Applied to the project, validating the backlog before saving it:
function isBacklogValid(tasks) {
return tasks.every((t) =>
typeof t.id === 'number' &&
typeof t.title === 'string' && t.title.trim() !== '' &&
t.estimatedHours > 0 && t.estimatedHours <= 40 && // R3
Array.isArray(t.tags)
);
}
console.log(isBacklogValid(backlog)); // true
console.log(isBacklogValid([{ id: 9, title: '', estimatedHours: 0, tags: [] }])); // false
- Selecting several:
filter
filterfilter(predicate) returns a new array with every element that meets the condition. It does not mutate the original and, unlike map, the length of the result does change.
const open = backlog.filter((t) => t.status !== 'done');
console.log(open.length); // 5
const openHighPriority = backlog.filter((t) => t.priority === 'high' && t.status !== 'done');
console.log(openHighPriority.map((t) => t.id)); // [ 1, 3, 6 ]
const ivanTasks = backlog.filter((t) => t.assignee === 'Iván');
console.log(ivanTasks.length); // 3
const empty = backlog.filter((t) => t.estimatedHours > 100);
console.log(empty); // [] ← an empty array, not undefinedThat last detail matters: filter never returns undefined, always an array (possibly empty). That is why filter(...).length === 0 is the correct check, and not if (result), which would always be true because an empty array is truthy (01-07).
Differences from find, which are often confused:
find |
filter |
|
|---|---|---|
| Returns | One element or undefined |
An array (possibly empty) |
| Does it walk the whole thing? | No: it stops at the first match | Yes, always |
| When to use it | You are after one specific item (by id) |
You want all the matches |
A frequent mistake is using filter when you wanted find:
// ✗ It walks all six tasks and returns a one-element array
const task = backlog.filter((t) => t.id === 3)[0];
// ✓ It stops as soon as it finds it and returns the task
const task2 = backlog.find((t) => t.id === 3);
- The searching table
| Method | What it returns | Does it stop early? | Use it when… |
|---|---|---|---|
find(p) |
The first matching element, or undefined |
Yes | You are after one specific task |
findIndex(p) |
Its position, or -1 |
Yes | You are going to modify the array by index |
findLast(p) |
The last match, or undefined |
Yes (from the end) | You care about the most recent one |
findLastIndex(p) |
Its position, or -1 |
Yes (from the end) | Same, but you need the position |
some(p) |
true / false |
Yes, at the first true |
"Is any of them overdue?" |
every(p) |
true / false |
Yes, at the first false |
"Are they all valid?" |
filter(p) |
An array with every match | No | You want a sublist |
includes(v) |
true / false |
Yes | You are after an exact primitive |
indexOf(v) |
Position or -1 |
Yes | You want the position of a primitive |
sort: the numbers trap
sort: the numbers trapLet us start with the disaster, because it is the best way to make sure you never forget it:
const numbers = [10, 9, 100, 1, 25];
numbers.sort();
console.log(numbers); // [ 1, 10, 100, 25, 9 ] ✗ what??This is not an engine bug. sort() with no arguments converts every element to a string and compares them alphabetically, character by character. Since '1' comes before '2' and '2' before '9', the result is dictionary order: '1', '10', '100', '25', '9'.
And there is a second, equally important trap: sort mutates the original array, as the 04-03 table already warned.
const hours = [12, 6, 14, 3, 8, 5];
const sorted = hours.sort();
console.log(hours === sorted); // true ← it is the SAME array, not a copy
console.log(hours); // [ 12, 14, 3, 5, 6, 8 ] ← the original, wreckedTwo rules follow from this, and you should always apply them:
- Always pass
sorta comparator function. No exceptions, not even with numbers. - Copy before sorting, unless you really do want to mutate:
array.slice().sort(cmp)or, better,array.toSorted(cmp).
- The comparator function
The comparator receives two elements and returns a number answering the question "which one comes first?":
| Returns | Meaning |
|---|---|
| A negative number | a comes before b |
| Zero | The order between them does not matter |
| A positive number | a comes after b |
const hours = [12, 6, 14, 3, 8, 5];
// Ascending: if a - b is negative, a is smaller and comes first
console.log(hours.toSorted((a, b) => a - b)); // [ 3, 5, 6, 8, 12, 14 ]
// Descending: flip the subtraction
console.log(hours.toSorted((a, b) => b - a)); // [ 14, 12, 8, 6, 5, 3 ]The a - b trick works because the subtraction already produces the right sign: if a is smaller, the subtraction is negative. It only works with numbers; with strings you have to compare some other way (section 8).
A comparator written out the long way, to see what happens inside:
function byHoursAscending(a, b) {
if (a.estimatedHours < b.estimatedHours) return -1;
if (a.estimatedHours > b.estimatedHours) return 1;
return 0;
}And a warning: the comparator must be consistent. If you say A comes before B and also that B comes before A, the result is unpredictable. Never use Math.random() inside a comparator to "shuffle": it can produce biased orderings or even errors.
- Sorting by number, by text and by date
By number (the case of estimatedHours and id):
const byHours = backlog.toSorted((a, b) => a.estimatedHours - b.estimatedHours);
console.log(byHours.map((t) => `${t.estimatedHours}h`).join(' '));
// 3h 5h 6h 8h 12h 14hBy text. Subtraction is no use with strings ('a' - 'b' is NaN). And the < operator compares by character code, which misplaces capitals and accents. The correct answer is localeCompare, which compares according to the language's rules:
console.log('á'.localeCompare('b', 'en')); // -1 ← 'á' comes before 'b'
console.log('á' < 'b'); // false ✗ comparison by code point
const byTitle = backlog.toSorted((a, b) => a.title.localeCompare(b.title, 'en'));
console.log(byTitle.map((t) => t.id)); // [ 5, 6, 1, 4, 2, 3 ]
console.log(byTitle[0].title); // 'Bookbinding guide for residents'
console.log(byTitle[2].title); // 'Redesign the multipurpose room'localeCompare returns a negative number, zero or a positive number: exactly what sort needs. The second argument is the language; the third accepts options such as { sensitivity: 'base' } to ignore case and accents:
const names = ['Iván', 'ana', 'Álvaro', 'Bruno'];
console.log(names.toSorted((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' })));
// [ 'Álvaro', 'ana', 'Bruno', 'Iván' ]By date. Here a design decision from Module 1 pays off: dueDate is an ISO string 'yyyy-mm-dd', and that format sorts alphabetically exactly as it sorts chronologically. So comparing strings is enough:
const byDueDate = backlog.toSorted((a, b) => a.dueDate.localeCompare(b.dueDate));
console.log(byDueDate.map((t) => `${t.dueDate} (${t.id})`).join('\n'));
// 2026-09-05 (6)
// 2026-09-12 (4)
// 2026-09-30 (1)
// 2026-10-02 (3)
// 2026-10-15 (2)
// 2026-11-05 (5)With the format '30/09/2026' this would be impossible without converting each date first. That is exactly why the model chose ISO.
- Descending order and sorting by several criteria
To reverse the order, reverse the comparator (do not sort and then apply reverse, which is two passes for nothing):
const largestFirst = backlog.toSorted((a, b) => b.estimatedHours - a.estimatedHours);
console.log(largestFirst[0].estimatedHours); // 14When there are ties, you move to the next criterion. The technique is to evaluate the first criterion and, if it gives 0, return the second:
const PRIORITY_ORDER = { high: 0, medium: 1, low: 2 };
function byPriorityThenDate(a, b) {
const byPriority = PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
if (byPriority !== 0) return byPriority;
return a.dueDate.localeCompare(b.dueDate); // tie-break by date
}
const board = backlog.toSorted(byPriorityThenDate);
console.log(board.map((t) => `${t.priority} ${t.dueDate} [${t.id}]`).join('\n'));
// high 2026-09-05 [6]
// high 2026-09-30 [1]
// high 2026-10-02 [3]
// medium 2026-10-15 [2]
// medium 2026-11-05 [5]
// low 2026-09-12 [4]Notice the PRIORITY_ORDER object: it is the same lookup dictionary pattern from 04-01, used here to turn a piece of text ('high') into a sortable number. Without it, 'high' < 'low' < 'medium' would give alphabetical order, which is not what Marta wants.
With more than two criteria, the pattern generalizes by chaining with ||, taking advantage of 0 being falsy:
const byThreeCriteria = (a, b) =>
(PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]) ||
a.dueDate.localeCompare(b.dueDate) ||
a.title.localeCompare(b.title, 'en');Read it like this: "use the first criterion; if it gives 0, try the second; if that gives 0 too, the third". It is a direct application of the || short circuit you studied in 01-06.
One last detail: since ES2019, sort is stable, which means elements that tie keep their original order. That is why sorting first by date and then by priority also produces a correct result, although the combined criterion is clearer.
toSorted and toReversed: sorting without mutating
toSorted and toReversed: sorting without mutatingYou have been using them throughout the previous section. They are the immutable versions of sort and reverse, available in modern JavaScript:
const original = [12, 6, 14];
const sorted = original.toSorted((a, b) => a - b);
console.log(sorted); // [ 6, 12, 14 ]
console.log(original); // [ 12, 6, 14 ] ✓ untouched
const reversed = original.toReversed();
console.log(reversed); // [ 14, 6, 12 ]
console.log(original); // [ 12, 6, 14 ] ✓ untouched| Mutating | Immutable | What it does |
|---|---|---|
sort(cmp) |
toSorted(cmp) |
Sorts |
reverse() |
toReversed() |
Reverses |
splice(i, n, v) |
toSpliced(i, n, v) |
Deletes/inserts |
arr[i] = v |
with(i, v) |
Replaces one position |
If you work in an older environment that does not have them, the equivalent is to copy first: array.slice().sort(cmp). What you must never do is sort the backlog itself just to display it a particular way: the data list and the presentation list are different things.
// ✗ It really sorts the backlog; the rest of the application sees a different order
backlog.sort((a, b) => a.estimatedHours - b.estimatedHours);
// ✓ It produces a sorted view without touching the data
const view = backlog.toSorted((a, b) => a.estimatedHours - b.estimatedHours);
reduce, slowly
reduce, slowlyreduce is the most powerful method in the arsenal and the one most poorly explained. The idea, in one sentence:
reducewalks the array carrying an accumulated value, and returns that value at the end.
Nothing more, nothing less. It is exactly the accumulator pattern you learned in 02-02, packaged into a method. Its signature has two parts:
The simplest version, adding up hours, compared with the equivalent loop:
// With a loop (what you already knew how to do)
let total = 0;
for (const task of backlog) {
total += task.estimatedHours;
}
console.log(total); // 48
// With reduce (the same thing, compressed)
const total2 = backlog.reduce((acc, task) => acc + task.estimatedHours, 0);
console.log(total2); // 48The piece-by-piece correspondence:
| In the loop | In reduce |
|---|---|
let total = 0 |
The initial value, the second argument |
total += ... |
Whatever the reducer returns |
The total variable at the end |
The return value of reduce |
Let us follow the execution pass by pass, which is the only way to really understand it:
| Pass | acc coming in |
task.estimatedHours |
Returns |
|---|---|---|---|
| 1 | 0 (initial) | 12 | 12 |
| 2 | 12 | 6 | 18 |
| 3 | 18 | 14 | 32 |
| 4 | 32 | 3 | 35 |
| 5 | 35 | 8 | 43 |
| 6 | 43 | 5 | 48 |
flowchart LR
I["initial: 0"] --> A["+12 → 12"] --> B["+6 → 18"] --> C["+14 → 32"]
C --> D["+3 → 35"] --> E["+8 → 43"] --> F["+5 → 48"]
Why the initial value matters so much. If you leave it out, reduce uses the first element as the initial accumulator and starts walking from the second. With loose numbers that still works; with objects, it does not:
console.log([12, 6, 14].reduce((a, b) => a + b)); // 32 ✓ it works by accident
// ✗ With no initial value, acc is the whole task object on the first pass
console.log(backlog.reduce((acc, t) => acc + t.estimatedHours));
// '[object Object]6143850' ← string concatenation, not a sumAnd there is a worse case still: with no initial value, an empty array throws an error.
console.log([].reduce((a, b) => a + b)); // ✗ TypeError: Reduce of empty array with no initial value
console.log([].reduce((a, b) => a + b, 0)); // ✓ 0Rule with no exceptions: always supply the initial value. And choose it carefully:
0to add,1to multiply,''to concatenate,[]to build a list,{}to build an object.
The other classic mistake, already flagged in 03-06, is forgetting to return the accumulator:
// ✗ The function returns nothing: acc becomes undefined on pass 2
const wrong = backlog.reduce((acc, t) => { acc.push(t.title); }, []);
// ✗ TypeError: Cannot read properties of undefined (reading 'push')
// ✓ Always return the accumulator
const good = backlog.reduce((acc, t) => { acc.push(t.title); return acc; }, []);
- Four real cases of
reduce
reduceCase 1: summing with a condition and a weight. The backlog's weighted effort, the number 124 you have been carrying since Module 3:
const effort = backlog.reduce((acc, t) => acc + WEIGHTS[t.priority] * t.estimatedHours, 0);
console.log(effort); // 124
// And the open hours, filtering inside the reduce itself
const openHours = backlog.reduce(
(acc, t) => (t.status === 'done' ? acc : acc + t.estimatedHours),
0
);
console.log(openHours); // 45Case 2: counting by status. The accumulator is an object:
const byStatus = backlog.reduce((acc, t) => {
acc[t.status] = (acc[t.status] ?? 0) + 1;
return acc;
}, {});
console.log(byStatus); // { 'in-progress': 2, pending: 3, done: 1 }Two details of the code: acc[t.status] uses brackets because the key is in a variable (04-01), and (acc[...] ?? 0) handles the first occurrence, when the key does not exist yet and would return undefined.
Case 3: grouping tasks by assignee. The accumulator is an object whose values are arrays:
const byAssignee = backlog.reduce((acc, t) => {
if (!acc[t.assignee]) acc[t.assignee] = [];
acc[t.assignee].push(t);
return acc;
}, {});
console.log(Object.keys(byAssignee)); // [ 'Iván', 'Marta', 'Lucía' ]
console.log(byAssignee['Iván'].length); // 3
console.log(byAssignee['Iván'].map((t) => t.id)); // [ 1, 5, 6 ]
console.log(byAssignee['Lucía'][0].title); // 'Update the bookings website'And the variant Marta cares about, grouping while adding up hours instead of accumulating tasks:
const hoursPerPerson = backlog.reduce((acc, t) => {
if (t.status === 'done') return acc;
acc[t.assignee] = (acc[t.assignee] ?? 0) + t.estimatedHours;
return acc;
}, {});
console.log(hoursPerPerson); // { Iván: 25, Marta: 6, Lucía: 14 }25 + 6 + 14 = 45, the backlog's open hours. It is exactly what you did by hand with loops in exercise 3 of 04-02, now in five lines.
Case 4: building an index by id. The dictionary you assembled with a for in 04-01:
const index = backlog.reduce((acc, t) => {
acc[t.id] = t;
return acc;
}, {});
console.log(index[3].title); // 'Update the bookings website'
console.log(index[99]?.title); // undefinedWhy bother? Because backlog.find((t) => t.id === 3) walks the list every time you search, whereas index[3] is a direct access. With six tasks it makes no difference; in Module 9 you will see when it starts to matter.
A style warning, one we also made in 03-06 about higher-order functions: reduce is not always the best option. If what you write with reduce is harder to read than the equivalent loop, write the loop. reduce shines at sums, counts, groupings and indexes; it becomes unreadable when someone uses it to do what map or filter do.
Object.groupBy, Map and Set
Object.groupBy, Map and SetObject.groupBy (modern JavaScript) does exactly case 3 without reduce:
const byPriority = Object.groupBy(backlog, (t) => t.priority);
console.log(Object.keys(byPriority)); // [ 'high', 'medium', 'low' ]
console.log(byPriority.high.map((t) => t.id)); // [ 1, 3, 6 ]
console.log(byPriority.medium.length); // 2
console.log(byPriority.low[0].title); // 'Screen-printing ink inventory'It is more readable than the equivalent reduce, but it is recent: check your environment's support before using it in production (in Module 8 you will see how such things are checked). There is also Map.groupBy, which returns a Map instead of an object.
Set is a collection with no duplicates. It is the answer to the question we left open in 04-03: how to deduplicate the backlog's tags.
const allTags = backlog.flatMap((t) => t.tags);
console.log(allTags.length); // 12
const uniqueTags = new Set(allTags);
console.log(uniqueTags.size); // 11 ← 'screen-printing' was there twice
console.log(uniqueTags.has('screen-printing')); // true
console.log(uniqueTags.has('marketing')); // false
// Turning it back into an array (with Array.from, from 04-03)
const uniqueList = Array.from(uniqueTags).toSorted((a, b) => a.localeCompare(b, 'en'));
console.log(uniqueList);
// [ 'bookbinding', 'bookings', 'carpentry', 'communication', 'design', 'documentation',
// 'purchasing', 'screen-printing', 'space', 'storeroom', 'web' ]The idiom Array.from(new Set(array)) is the standard way of removing duplicates in JavaScript, and it is worth memorizing.
Map is a dictionary whose keys can be of any type, which preserves insertion order and knows how many entries it holds:
const counts = new Map();
for (const t of backlog) {
counts.set(t.assignee, (counts.get(t.assignee) ?? 0) + 1);
}
console.log(counts.get('Iván')); // 3
console.log(counts.size); // 3
console.log(Array.from(counts.entries()));
// [ [ 'Iván', 3 ], [ 'Marta', 2 ], [ 'Lucía', 1 ] ]When to choose each structure:
| You need | Use |
|---|---|
| To group the named fields of an entity | An object literal |
| A dictionary with known text keys | An object literal |
| A dictionary with dynamic keys, or to keep a count | Map |
| Keys that are not strings (real numbers, objects) | Map |
| A collection with no duplicates | Set |
- Chaining
filter().map().sort() and its cost
filter().map().sort() and its costSince filter, map, toSorted and flatMap return arrays, they can be chained, and the result reads almost like a sentence:
const highPriorityReport = backlog
.filter((t) => t.priority === 'high' && t.status !== 'done')
.toSorted((a, b) => a.dueDate.localeCompare(b.dueDate))
.map((t) => `${t.dueDate} · ${t.title} (${t.assignee}, ${t.estimatedHours} h)`);
console.log(highPriorityReport.join('\n'));
// 2026-09-05 · Carpentry workshop quote (Iván, 5 h)
// 2026-09-30 · Redesign the multipurpose room (Iván, 12 h)
// 2026-10-02 · Update the bookings website (Lucía, 14 h)Read it top to bottom: keep the open high-priority ones, sort them by date, turn them into text. Each step has a name and can be read on its own, which is exactly what you were after in 03-06 with filter → transform → aggregate.
The cost: each link in the chain walks the whole array and creates a new one. Three chained methods are three walks and two intermediate arrays, whereas a for would do a single pass without creating anything.
| Chain of three methods | Single loop | |
|---|---|---|
| Walks | 3 | 1 |
| Intermediate arrays | 2 | 0 |
| Readability | Very high | Medium |
With six tasks, the difference is literally imperceptible. With hundreds of thousands of elements it starts to show, and that is where the techniques of Optimizing JavaScript Performance come in. The advice, today: chain and prioritize readability, and optimize only when you measure that it is needed. One free detail: put the filter first in the chain, so the following steps work on fewer elements.
- Worked example: the weekly workload report
Marta opens the panel every Monday and wants a complete snapshot of the backlog. With what you have learned, it fits into one function.
'use strict';
function weeklyReport(tasks, today) {
const open = tasks.filter((t) => t.status !== 'done');
const totals = {
tasks: tasks.length,
open: open.length,
totalHours: tasks.reduce((acc, t) => acc + t.estimatedHours, 0),
openHours: open.reduce((acc, t) => acc + t.estimatedHours, 0),
effort: tasks.reduce((acc, t) => acc + WEIGHTS[t.priority] * t.estimatedHours, 0)
};
const workload = open.reduce((acc, t) => {
acc[t.assignee] = (acc[t.assignee] ?? 0) + t.estimatedHours;
return acc;
}, {});
const overdue = open
.filter((t) => t.dueDate < today)
.toSorted((a, b) => a.dueDate.localeCompare(b.dueDate));
const upcoming = open
.toSorted((a, b) => a.dueDate.localeCompare(b.dueDate))
.slice(0, 3);
const tags = Array.from(new Set(tasks.flatMap((t) => t.tags)))
.toSorted((a, b) => a.localeCompare(b, 'en'));
return { totals, workload, overdue, upcoming, tags, atRisk: overdue.length > 0 };
}
function printReport(report, today) {
const { totals, workload, overdue, upcoming, tags } = report;
console.log(`═══ Weekly report · Taller Nómada · ${today} ═══`);
console.log(`${totals.open} of ${totals.tasks} tasks open · ` +
`${totals.openHours} h of ${totals.totalHours} h · ` +
`weighted effort ${totals.effort}`);
console.log('\nWorkload per person:');
Object.entries(workload)
.toSorted((a, b) => b[1] - a[1])
.forEach((pair) => console.log(` ${pair[0].padEnd(6)} ${pair[1]} h`));
console.log(`\nOverdue (${overdue.length}):`);
if (overdue.length === 0) console.log(' — none —');
overdue.forEach((t) => console.log(` ⚠ ${t.dueDate} ${t.title} (${t.assignee})`));
console.log('\nUpcoming due dates:');
upcoming.forEach((t) => console.log(` ${t.dueDate} ${t.title}`));
console.log(`\nTags in use (${tags.length}): ${tags.join(', ')}`);
}
printReport(weeklyReport(backlog, TODAY), TODAY);Output:
═══ Weekly report · Taller Nómada · 2026-09-20 ═══ 5 of 6 tasks open · 45 h of 48 h · weighted effort 124 Workload per person: Iván 25 h Lucía 14 h Marta 6 h Overdue (1): ⚠ 2026-09-05 Carpentry workshop quote (Iván) Upcoming due dates: 2026-09-05 Carpentry workshop quote 2026-09-30 Redesign the multipurpose room 2026-10-02 Update the bookings website Tags in use (11): bookbinding, bookings, carpentry, communication, design, documentation, purchasing, screen-printing, space, storeroom, web
Go over which method solves each block: filter for the open and overdue ones, reduce for the three totals and for the workload per person, toSorted with localeCompare for ordering by date and by text, slice(0, 3) to keep the next three due dates, flatMap + Set for the unique tags, and Object.entries + toSorted to order the workload object by descending hours. Compare it with the report engine you assembled in 03-06 over parallel arrays: the logic is the same, but it fits on one screen and it reads.
(The line const { totals, workload, ... } = report; is object destructuring, and it is the subject of 04-07. For now read it as "pull these properties out into separate variables".)
Common Mistakes and Tips
1. Calling sort() with no comparator. It sorts as text: [10, 9, 100] becomes [10, 100, 9]. Always pass (a, b) => a - b or whatever comparator fits.
2. Forgetting that sort and reverse mutate. Use toSorted/toReversed, or copy with slice() first.
3. Omitting the initial value of reduce. With objects it produces garbage and with empty arrays it throws a TypeError.
4. Forgetting the return in the reducer. The accumulator becomes undefined and everything blows up on the next pass.
5. Using filter(...)[0] instead of find(...). It walks the entire list just to keep one item.
6. Checking the result of filter with if (result). An empty array is truthy: it will always pass. Check result.length === 0.
7. Comparing strings with < instead of localeCompare. With accents and capitals you get incorrect orderings.
8. Using every over an empty list as validation. It always returns true. Check first that the list is not empty if that matters.
9. Chaining ten methods over huge lists. Each link is a walk and a new array. Readability first; optimize when you measure (Module 9).
Professional tip. These methods are a vocabulary, and using it well is above all about saying what you want. If the method's name matches what you would say out loud —"find me the first one", "keep the high-priority ones", "add up the hours"— the code is fine. If you have to explain a twelve-line reduce, there was probably a loop there.
Exercises
Exercise 1 — Monday's queries. With the canonical backlog, solve each point in a single expression:
- The task with
id5. - Is there any of Lucía's tasks with no reviewer?
- Do all the open tasks have at least one tag?
- The titles of the medium-priority tasks.
- The last task (walking from the end) that is in progress.
Exercise 2 — The workload ranking. Write workloadRanking(tasks) returning an array of objects { assignee, tasks, hours, overdue } with one entry per person, counting open tasks only, sorted by descending hours and, in case of a tie, by name. Use reduce to group and toSorted with a double criterion. Check that Iván comes first with 25 h and 1 overdue task.
Exercise 3 — Balanced distribution. Marta wants to know whether the work is evenly spread. Write isBalanced(tasks, headroom) returning true if the difference between the open hours of the most loaded and the least loaded person does not exceed headroom. Also return, in an object, { balanced, max, min, difference }. Try it with headroom = 10 and with headroom = 20.
Solutions
Exercise 1
// 1. find: you are after ONE specific task
console.log(backlog.find((t) => t.id === 5).title);
// 'Bookbinding guide for residents'
// 2. some: one is enough
console.log(backlog.some((t) => t.assignee === 'Lucía' && t.reviewer === null));
// false ← Lucía's task 3 does have a reviewer (Iván)
// 3. every over the subset of open tasks
console.log(backlog.filter((t) => t.status !== 'done').every((t) => t.tags.length > 0));
// true
// 4. filter + map: select and transform
console.log(backlog.filter((t) => t.priority === 'medium').map((t) => t.title));
// [ 'Signage for the screen-printing workshop', 'Bookbinding guide for residents' ]
// 5. findLast: walks from the end
console.log(backlog.findLast((t) => t.status === 'in-progress').title);
// 'Bookbinding guide for residents'Frequent mistakes in this exercise: using filter in point 1 (it walks too far and returns an array), and in point 3 applying every to the whole backlog instead of to the open tasks, which answers a different question.
Exercise 2
function workloadRanking(tasks, today = TODAY) {
const byPerson = tasks
.filter((t) => t.status !== 'done')
.reduce((acc, t) => {
if (!acc[t.assignee]) {
acc[t.assignee] = { assignee: t.assignee, tasks: 0, hours: 0, overdue: 0 };
}
const row = acc[t.assignee];
row.tasks += 1;
row.hours += t.estimatedHours;
if (t.dueDate < today) row.overdue += 1;
return acc;
}, {});
return Object.values(byPerson).toSorted(
(a, b) => (b.hours - a.hours) || a.assignee.localeCompare(b.assignee, 'en')
);
}
console.log(workloadRanking(backlog));
// [ { assignee: 'Iván', tasks: 3, hours: 25, overdue: 1 },
// { assignee: 'Lucía', tasks: 1, hours: 14, overdue: 0 },
// { assignee: 'Marta', tasks: 1, hours: 6, overdue: 0 } ]Three things worth commenting on. First: the accumulator is an object of objects, and at the end Object.values turns it into the array the brief asks for. Second: inside the reduce the accumulator object is mutated, and that is fine because that object was created inside the function itself and nobody else shares it; mutating is a problem when it affects data from outside. Third: notice that Marta shows up with 1 task and 6 h, not 2: her "Screen-printing ink inventory" is done and the initial filter discards it.
Exercise 3
function isBalanced(tasks, headroom) {
const hours = tasks
.filter((t) => t.status !== 'done')
.reduce((acc, t) => {
acc[t.assignee] = (acc[t.assignee] ?? 0) + t.estimatedHours;
return acc;
}, {});
const values = Object.values(hours);
if (values.length === 0) {
return { balanced: true, max: 0, min: 0, difference: 0 };
}
const max = values.reduce((a, b) => (b > a ? b : a), values[0]);
const min = values.reduce((a, b) => (b < a ? b : a), values[0]);
const difference = max - min;
return { balanced: difference <= headroom, max, min, difference };
}
console.log(isBalanced(backlog, 10));
// { balanced: false, max: 25, min: 6, difference: 19 }
console.log(isBalanced(backlog, 20));
// { balanced: true, max: 25, min: 6, difference: 19 }Two notes. The maximum pattern with reduce is the same one from 02-02, and the initial value is values[0] instead of 0 because with a fixed 0 the minimum would always come out as 0. And the empty-list check is essential: without it, values[0] would be undefined and every comparison would be false. That kind of edge case is exactly what you will test systematically in Module 8 with Jest.
With headroom = 10 the distribution is not balanced: Iván piles up 25 h against Marta's 6 h. That is precisely the diagnosis Marta needed in order to redistribute the backlog.
Conclusion
You now have the full arsenal for querying a list. For searching: find and findIndex when you want an element or its position, findLast and findLastIndex when you care about the most recent one, some and every when the answer is a yes or a no —with their short circuit and with the empty-array trap—, and filter when you want a sublist, remembering that it always returns an array and that an empty array is truthy.
For sorting, sort with its two traps burned into memory: with no comparator it sorts as text ([10, 9, 100] → [10, 100, 9]) and it mutates the original. The comparator returns negative, zero or positive; a - b for numbers, localeCompare for text, plain string comparison for ISO dates —the reward for that Module 1 design decision—, the flipped subtraction for descending order and chaining with || for several criteria. And toSorted/toReversed so as not to destroy the data while building a view.
For aggregating, reduce, genuinely understood: it is the accumulator pattern with a name, its reducer must always return the accumulator and its initial value is not optional. You have used it in the four cases that cover 90% of what you will need: summing (48 h, 45 h open, effort 124), counting by status, grouping by assignee (Iván 25 h, Lucía 14 h, Marta 6 h) and building an index by id. Alongside it, Object.groupBy for effortless grouping, Set for deduplicating the 12 tags down to 11 unique ones and Map for dictionaries with dynamic keys. And you know how to chain filter().toSorted().map() while knowing what each link costs. The Taller Nómada weekly workload report, the deliverable outstanding since Module 1, is now written.
If anything has recurred suspiciously in the last few pages, it is lines like pair[0] and pair[1] when walking Object.entries, or const [removed] = tasks.splice(...), or const { totals, workload } = report. They all point to the same thing: there is a syntax for unpacking an array or an object into named variables, and you have been using it in passing for three lessons without ever studying it. We start with the easier half in Array Destructuring, where you will learn to split a dueDate into year, month and day in a single line, to swap variables without a helper and to finally write for (const [i, task] of backlog.entries()) understanding exactly what it means.
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
