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

  1. The starting point: the canonical backlog
  2. Finding one element: find, findIndex, findLast
  3. Checking the whole set: some and every
  4. Selecting several: filter
  5. The searching table
  6. sort: the numbers trap
  7. The comparator function
  8. Sorting by number, by text and by date
  9. Descending order and sorting by several criteria
  10. toSorted and toReversed: sorting without mutating
  11. reduce, slowly
  12. Four real cases of reduce
  13. Object.groupBy, Map and Set
  14. Chaining filter().map().sort() and its cost
  15. Worked example: the weekly workload report
  16. Common Mistakes and Tips
  17. Exercises
  18. Conclusion

  1. 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.

  1. Finding one element: find, findIndex, findLast

find(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);               // undefined

Compare 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 ??:

console.log(backlog.find((t) => t.assignee === 'Nobody')?.title ?? 'no results');
// 'no results'

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));       // -1

It 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 one

Remember, 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  ✓

  1. Checking the whole set: some and every

Sometimes 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 match

That 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

  1. Selecting several: filter

filter(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 undefined

That 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);

  1. 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

  1. sort: the numbers trap

Let 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, wrecked

Two rules follow from this, and you should always apply them:

  1. Always pass sort a comparator function. No exceptions, not even with numbers.
  2. Copy before sorting, unless you really do want to mutate: array.slice().sort(cmp) or, better, array.toSorted(cmp).

  1. 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.

  1. 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 14h

By 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.

  1. 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);   // 14

When 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.

  1. toSorted and toReversed: sorting without mutating

You 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);

  1. reduce, slowly

reduce is the most powerful method in the arsenal and the one most poorly explained. The idea, in one sentence:

reduce walks 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:

array.reduce(reducer, initialValue)
//           ↑ (accumulated, element, index, array) => newAccumulated

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);      // 48

The 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 sum

And 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));    // ✓ 0

Rule with no exceptions: always supply the initial value. And choose it carefully: 0 to add, 1 to 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; }, []);

  1. Four real cases of reduce

Case 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);     // 45

Case 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);  // undefined

Why 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.

  1. Object.groupBy, Map and Set

Object.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

  1. Chaining filter().map().sort() and its cost

Since 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.

  1. 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:

  1. The task with id 5.
  2. Is there any of Lucía's tasks with no reviewer?
  3. Do all the open tasks have at least one tag?
  4. The titles of the medium-priority tasks.
  5. 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

Module 2: Control Structures

Module 3: Functions

Module 4: Objects and Arrays

Module 5: Advanced Objects and Functions

Module 6: The Document Object Model (DOM)

Module 7: Browser APIs and Advanced Topics

Module 8: Testing and Debugging

Module 9: Performance and Optimization

Module 10: JavaScript Frameworks and Libraries

Module 11: Final Project

© Copyright 2026. All rights reserved