The previous lesson ended with an uncomfortable observation: nearly every function you wrote walked the list by hand with a loop, and not always the same one. That is normal, because JavaScript offers six different ways of walking an array and none of them is the best in every case. Choosing well is not a matter of taste: some forms give you the index and others do not, some can be interrupted halfway and others cannot, some return a result and some only produce effects. In this lesson you will see all six with their exact rules, you will build the comparison table that settles the choice in five seconds, and you will apply them to two real Nómada Tasks jobs: printing the Taller Nómada board to the console and turning the backlog into report lines for Marta.

Contents

  1. The six forms at a glance
  2. The classic for: total control
  3. for...of: the default option
  4. for...in over arrays: the frequent mistake
  5. forEach: walking with a function
  6. Why forEach cannot be broken out of
  7. entries(), keys() and values()
  8. map: walking in order to transform
  9. The comparison table
  10. Walking arrays of objects and nested arrays
  11. The mistake of mutating the array while walking it
  12. Worked example: printing the Taller Nómada board
  13. Common Mistakes and Tips
  14. Exercises
  15. Conclusion

  1. The six forms at a glance

Before the details, here are the six of them walking the same array:

const assignees = ['Iván', 'Marta', 'Lucía'];

// 1. classic for
for (let i = 0; i < assignees.length; i++) console.log(assignees[i]);

// 2. for...of
for (const person of assignees) console.log(person);

// 3. for...in  (over arrays it is nearly always a mistake!)
for (const index in assignees) console.log(assignees[index]);

// 4. forEach
assignees.forEach((person) => console.log(person));

// 5. entries() to get index and value
for (const pair of assignees.entries()) console.log(pair[0], pair[1]);

// 6. map (walks while transforming)
const uppercased = assignees.map((person) => person.toUpperCase());

All six print (or produce) what you would expect with this array. The differences show up as soon as you need the index, want to leave early, the array has holes, or there are asynchronous operations inside the loop.

flowchart TD
    A["I want to walk an array"] --> B{"Do I need<br/>to transform it?"}
    B -->|Yes| C["map"]
    B -->|No| D{"Do I need<br/>to interrupt?"}
    D -->|Yes| E["for...of<br/>(or classic for)"]
    D -->|No| F{"Do I need<br/>the index?"}
    F -->|Yes| G["for...of + entries()"]
    F -->|No| H["for...of or forEach"]

  1. The classic for: total control

You have known it since 02-02. Its virtue is that you control the counter, so you can do things no other form allows.

const tasks = ['T1', 'T2', 'T3', 'T4', 'T5', 'T6'];

// Normal walk
for (let i = 0; i < tasks.length; i++) {
  console.log(`${i + 1}. ${tasks[i]}`);
}

// Backwards
for (let i = tasks.length - 1; i >= 0; i--) {
  console.log(tasks[i]);       // T6, T5, T4...
}

// Two at a time
for (let i = 0; i < tasks.length; i += 2) {
  console.log(tasks[i]);       // T1, T3, T5
}

// Comparing each element with the next one
for (let i = 0; i < tasks.length - 1; i++) {
  console.log(`${tasks[i]} comes before ${tasks[i + 1]}`);
}

Those four cases —backwards, with a step, stopping before the end, looking at the neighbor— are why the classic for is still alive. Outside them it is noisy: you have to write three expressions, declare a variable that serves only as a counter and access with tasks[i] instead of working with the element directly. And it has a risk of its own: the off-by-one, that is, writing <= instead of < and reading undefined on the last pass.

for (let i = 0; i <= tasks.length; i++) {   // ✗ one pass too many
  console.log(tasks[i]);                    // the last one prints undefined
}

  1. for...of: the default option

for...of walks the array's values, with no counter and no indexes.

const backlog = [
  { id: 1, title: 'Redesign the multipurpose room',           assignee: 'Iván',  estimatedHours: 12, status: 'in-progress' },
  { id: 2, title: 'Signage for the screen-printing workshop', assignee: 'Marta', estimatedHours: 6,  status: 'pending' },
  { id: 3, title: 'Update the bookings website',              assignee: 'Lucía', estimatedHours: 14, status: 'pending' },
  { id: 4, title: 'Screen-printing ink inventory',            assignee: 'Marta', estimatedHours: 3,  status: 'done' },
  { id: 5, title: 'Bookbinding guide for residents',          assignee: 'Iván',  estimatedHours: 8,  status: 'in-progress' },
  { id: 6, title: 'Carpentry workshop quote',                 assignee: 'Iván',  estimatedHours: 5,  status: 'pending' }
];

for (const task of backlog) {
  console.log(`${task.title} — ${task.assignee}`);
}

Four reasons why it is the default option:

  1. There are no indexes to get wrong. The i, the <, the ++ and the off-by-one risk all disappear.
  2. It can be interrupted with break, continue and return, just like a classic for.
  3. const genuinely works. Each pass creates a new variable, so you can declare it const (impossible in the classic for, where the counter changes).
  4. It works with any iterable, not just arrays: strings, Map, Set, the DOM's NodeList (Module 6)…
// break and continue work as normal
for (const task of backlog) {
  if (task.status === 'done') continue;         // skip the finished ones
  if (task.estimatedHours > 12) {
    console.log(`Too big: ${task.title}`);
    break;                                       // leave the loop
  }
}

Its only shortcoming is that it does not give the index. That is what entries() is for (section 7).

  1. for...in over arrays: the frequent mistake

for...in is meant for objects, as you saw in 04-01: it walks the keys. Over an array it walks the indexes, but as strings, and that creates two problems.

const hours = [12, 6, 14];

for (const i in hours) {
  console.log(typeof i, i, hours[i]);
}
// string 0 12
// string 1 6
// string 2 14

Problem 1: the indexes are strings. + with strings concatenates, so any arithmetic on the index fails silently:

for (const i in hours) {
  console.log(`Task ${i + 1}`);     // ✗ 'Task 01', 'Task 11', 'Task 21'
}

Problem 2: it walks every enumerable property, not just the indexes. Including any that someone has added to the array or to the prototype:

const tags = ['space', 'design'];
tags.author = 'Lucía';

for (const key in tags) console.log(key);
// 0
// 1
// author      ← ✗ it is not an element

for (const value of tags) console.log(value);
// space
// design      ← ✓ elements only

There is a third detail: for...in skips the holes of a sparse array, whereas for...of walks them and returns undefined. Added to the above, the conclusion is blunt:

for...in for objects; for...of for arrays. If you see a for...in over an array in someone else's code, it is nearly always a bug or code from before 2015.

  1. forEach: walking with a function

forEach is the first of the higher-order methods you studied in 03-06, and now you get to use the native version. It takes a callback that it calls once per element, with three arguments: value, index and the whole array.

backlog.forEach((task, index, array) => {
  console.log(`${index + 1} of ${array.length}: ${task.title}`);
});
// 1 of 6: Redesign the multipurpose room
// 2 of 6: Signage for the screen-printing workshop
// ...

In practice the third argument is almost never used, and often the second one is not either:

backlog.forEach((task) => console.log(task.title));

Its characteristics, the good and the bad:

  • It returns undefined. It produces nothing; it is only good for effects (printing, saving, modifying something outside).
  • It gives the index effortlessly, unlike for...of.
  • It skips the holes of a sparse array.
  • It cannot be interrupted: that is the subject of the next section.
  • Each element is processed in a function call, with its own scope, which completely avoids the shared-variable problem you studied in 03-04 with var.

A typical use in the project: notifying the assignees of overdue tasks.

const TODAY = '2026-09-20';

backlog.forEach((task) => {
  if (task.dueDate < TODAY && task.status !== 'done') {
    console.log(`⚠ ${task.assignee}: "${task.title}" was due on ${task.dueDate}`);
  }
});

  1. Why forEach cannot be broken out of

This is the point that confuses people most. A break inside forEach is simply a syntax error:

backlog.forEach((task) => {
  if (task.id === 3) break;      // ✗ SyntaxError: Illegal break statement
});

The reason is structural. break exists to leave a loop, and inside forEach you are not in a loop: you are inside the body of a function that forEach invokes over and over. Remember how you implemented myForEach in 03-06:

function myForEach(array, callback) {
  for (let i = 0; i < array.length; i++) {
    callback(array[i], i, array);       // ← the loop is HERE, outside your function
  }
}

Your callback has no power over that for. And return inside the callback does not cut anything either: it simply ends that call and forEach moves on to the next element. It behaves like a continue, not like a break:

backlog.forEach((task) => {
  if (task.status === 'done') return;    // ← acts as a continue
  console.log(task.title);
});

What do you do when you genuinely need to stop?

Situation Solution
You need to stop as soon as you find something for...of with break
You are looking for one element find / findIndex (04-05)
You only want to know whether any matches some (04-05)
You want to know whether all match every (04-05)
You want to keep several filter (04-05)

And an antipattern you will see and must not copy: throwing an exception to force the exit.

// ✗ Never do this: using the error system as flow control
try {
  backlog.forEach((t) => { if (t.id === 3) throw new Error('found'); });
} catch (e) { }

It is slow, it hides real errors and it confuses whoever reads the code. If you need break, use for...of.

  1. entries(), keys() and values()

These three methods return iterators (a structure that produces values on demand; you will see them in depth in 05-08), designed to be used with for...of.

const assignees = ['Iván', 'Marta', 'Lucía'];

// keys(): the indexes
for (const i of assignees.keys()) console.log(i);          // 0, 1, 2

// values(): the values (equivalent to a plain for...of)
for (const v of assignees.values()) console.log(v);        // Iván, Marta, Lucía

// entries(): [index, value] pairs
for (const pair of assignees.entries()) {
  console.log(`${pair[0]} → ${pair[1]}`);
}
// 0 → Iván
// 1 → Marta
// 2 → Lucía

entries() fixes exactly what for...of lacks: it gives you index and value while keeping the option of using break. With destructuring —which you will study in 04-06— it is written the way it is actually used:

for (const [index, task] of backlog.entries()) {
  console.log(`${index + 1}. ${task.title}`);
  if (index === 2) break;             // ← here break DOES work
}
// 1. Redesign the multipurpose room
// 2. Signage for the screen-printing workshop
// 3. Update the bookings website

Compare the three alternatives when you need index and value:

// A. classic for: it works, but it is noisy
for (let i = 0; i < backlog.length; i++) console.log(i, backlog[i].title);

// B. forEach: readable, but it cannot be broken out of
backlog.forEach((t, i) => console.log(i, t.title));

// C. for...of + entries(): readable AND breakable   ← the best of the three
for (const [i, t] of backlog.entries()) console.log(i, t.title);

  1. map: walking in order to transform

map deserves a section of its own because it changes the purpose of the walk. forEach and for...of walk in order to do something; map walks in order to produce a new array with one element for each original element.

You implemented it by hand in 03-06 as myMap; the native one works the same way:

const titles = backlog.map((task) => task.title);
console.log(titles.length);     // 6
console.log(titles[0]);         // 'Redesign the multipurpose room'

const inMinutes = backlog.map((task) => task.estimatedHours * 60);
console.log(inMinutes);         // [ 720, 360, 840, 180, 480, 300 ]

console.log(backlog.length);    // 6  ← the original, untouched

Its three defining properties, worth committing to memory:

  1. The resulting array always has the same length as the original. map neither filters nor discards: if six go in, six come out. To keep fewer, use filter (04-05).
  2. It does not mutate the original. It returns a new one.
  3. The callback must return something. Forgetting the return is the number one mistake with map.
// ✗ The classic mistake: braces with no return
const wrong = backlog.map((task) => { task.title; });
console.log(wrong);      // [ undefined, undefined, undefined, undefined, undefined, undefined ]

// ✓ With an explicit return
const goodA = backlog.map((task) => { return task.title; });

// ✓ Or with an implicit return, no braces
const goodB = backlog.map((task) => task.title);

This distinction comes straight from 03-02: an arrow without braces returns the expression; an arrow with braces needs an explicit return.

Applied to the project: turning the backlog into report lines. Compare the same job with forEach and with map:

const BADGES = { done: '✓', 'in-progress': '▸', pending: '○' };

// With forEach: you have to create a helper array and push into it
const linesA = [];
backlog.forEach((task) => {
  linesA.push(`${BADGES[task.status]} [${task.id}] ${task.title}`);
});

// With map: the array is the result, no helper variables
const linesB = backlog.map((task) => `${BADGES[task.status]} [${task.id}] ${task.title}`);

console.log(linesB.join('\n'));
// ▸ [1] Redesign the multipurpose room
// ○ [2] Signage for the screen-printing workshop
// ○ [3] Update the bookings website
// ✓ [4] Screen-printing ink inventory
// ▸ [5] Bookbinding guide for residents
// ○ [6] Carpentry workshop quote

The map version is shorter, it does not need linesA declared beforehand, and above all it says what it does: "each task becomes a line". Style rule: if the only thing you do inside a forEach is push into an array, what you wanted was map.

  1. The comparison table

This is the lesson's reference table:

Form Does it give the index? Can it be broken out of? What does it return? And with await inside?
classic for Yes (you carry it) Yes (break, continue, return) Nothing Yes, it waits on each pass
for...of No (use entries()) Yes Nothing Yes, it waits on each pass
for...in Yes, but as a string Yes Nothing Yes (but do not use it with arrays)
forEach Yes (2nd argument) No undefined No: it does not wait for the callbacks
entries() + for...of Yes Yes Nothing Yes
map Yes (2nd argument) No A new array Not directly: it produces promises

The last two columns anticipate a real problem you will solve in Promises and Async/Await. A two-line preview: if you put an await inside a forEach, the forEach does not wait and the program carries on before the operations have finished; with for...of, by contrast, each pass waits for the previous one. You do not need to understand it yet, but you do need to know that the choice of loop will matter once asynchrony arrives.

How to choose, in three rules:

  1. Are you going to transform the list into another list? → map.
  2. Are you going to interrupt the walk, or is there asynchrony? → for...of (with entries() if you need the index).
  3. Do you only want effects on each element, with no early exit? → forEach or for...of, whichever the team prefers.

  1. Walking arrays of objects and nested arrays

An array of objects is walked in exactly the same way; the only difference is that inside the loop you work with properties:

for (const task of backlog) {
  console.log(`${task.title}: ${task.estimatedHours} h (${task.assignee})`);
}

When the object itself contains an array —such as tags— you get a loop inside another one, the 02-04 technique:

const taggedTasks = [
  { id: 1, title: 'Redesign the multipurpose room', tags: ['space', 'design'] },
  { id: 4, title: 'Screen-printing ink inventory', tags: ['screen-printing', 'storeroom'] },
  { id: 7, title: 'Service the paper guillotine', tags: [] }
];

for (const task of taggedTasks) {
  if (task.tags.length === 0) {
    console.log(`${task.title}: no tags`);
    continue;
  }
  for (const tag of task.tags) {
    console.log(`${task.title} → #${tag}`);
  }
}
// Redesign the multipurpose room → #space
// Redesign the multipurpose room → #design
// Screen-printing ink inventory → #screen-printing
// Screen-printing ink inventory → #storeroom
// Service the paper guillotine: no tags

Be careful with break in nested loops: it breaks only the innermost loop. If you need to leave both, use a loop label (02-04) or extract the double loop into a function and use return:

function findTaskWithTag(tasks, target) {
  for (const task of tasks) {
    for (const tag of task.tags) {
      if (tag === target) return task;      // leaves BOTH loops
    }
  }
  return null;
}

console.log(findTaskWithTag(taggedTasks, 'storeroom').id);   // 4

And for a matrix (an array of arrays) the pattern is the same:

const hoursPerWeek = [
  [8, 6, 7, 5, 4],      // week 1
  [6, 6, 8, 8, 2]       // week 2
];

for (const [w, week] of hoursPerWeek.entries()) {
  let total = 0;
  for (const hours of week) total += hours;
  console.log(`Week ${w + 1}: ${total} h`);
}
// Week 1: 30 h
// Week 2: 30 h

  1. The mistake of mutating the array while walking it

We already flagged it in 04-03; here it is time to understand it properly, because each way of walking reacts differently.

Deleting while walking with indexes: the elements shift and you skip some.

const tasks = ['a', 'b', 'b', 'c'];
for (let i = 0; i < tasks.length; i++) {
  if (tasks[i] === 'b') tasks.splice(i, 1);
}
console.log(tasks);       // [ 'a', 'b', 'c' ]   ✗ one 'b' is left

Deleting inside forEach: the same problem, and on top of that forEach fixes the range at the start, so it may read positions that no longer exist.

const tasks2 = ['a', 'b', 'b', 'c'];
tasks2.forEach((t, i) => { if (t === 'b') tasks2.splice(i, 1); });
console.log(tasks2);      // [ 'a', 'b', 'c' ]   ✗ just as wrong

Adding inside for...of: an infinite loop, because the iterator keeps finding new elements.

// ✗ DO NOT RUN THIS
// for (const t of tasks) { tasks.push(t); }

The three correct solutions, in order of preference:

// 1. The best one: do not mutate, create a new array (filter, lesson 04-05)
const withoutBs = ['a', 'b', 'b', 'c'].filter((t) => t !== 'b');
console.log(withoutBs);   // [ 'a', 'c' ]

// 2. Walk backwards: the pending indexes do not move
const tasks3 = ['a', 'b', 'b', 'c'];
for (let i = tasks3.length - 1; i >= 0; i--) {
  if (tasks3[i] === 'b') tasks3.splice(i, 1);
}
console.log(tasks3);      // [ 'a', 'c' ]   ✓

// 3. Walk a copy and modify the original
const tasks4 = ['a', 'b', 'b', 'c'];
for (const t of tasks4.slice()) {
  if (t === 'b') tasks4.splice(tasks4.indexOf(t), 1);
}
console.log(tasks4);      // [ 'a', 'c' ]   ✓

Golden rule: the array you are walking is read-only for as long as the walk lasts. If you have to change it, produce another array.

One nuance that is safe: modifying the objects inside does not change the array's structure and therefore does not break the walk.

for (const task of backlog) {
  if (task.dueDate < TODAY && task.status !== 'done') {
    task.priority = 'high';      // ✓ safe: it changes the object, not the list
  }
}

  1. Worked example: printing the Taller Nómada board

Marta wants to see the board in the console, grouped by status, numbered and with a summary footer. Each block of the code uses the way of walking that suits it best, and that is the real exercise of this lesson.

'use strict';

const TODAY = '2026-09-20';
const BADGES = { pending: '○', 'in-progress': '▸', done: '✓' };
const STATUS_ORDER = ['in-progress', 'pending', 'done'];
const STATUS_NAMES = { pending: 'NOT STARTED', 'in-progress': 'UNDER WAY', done: 'COMPLETED' };

/** Returns the line for one task. Uses map + join for the tags. */
function taskLine(task, today) {
  const overdue = task.dueDate < today && task.status !== 'done';
  const tags = task.tags.map((t) => `#${t}`).join(' ');
  return `${BADGES[task.status]} [${task.id}] ${task.title} · ${task.assignee} · ` +
         `${task.estimatedHours} h · ${tags}${overdue ? ' ⚠ OVERDUE' : ''}`;
}

function paintBoard(tasks, today) {
  console.log(`═══ Taller Nómada board — ${today} ═══`);

  // for...of over the statuses: a walk whose order we control
  for (const status of STATUS_ORDER) {
    // Here we need numbering within each block: an explicit counter
    let number = 0;
    const header = STATUS_NAMES[status];
    const lines = [];

    // for...of over the tasks, with continue to skip the ones in another status
    for (const task of tasks) {
      if (task.status !== status) continue;
      number++;
      lines.push(`  ${number}. ${taskLine(task, today)}`);
    }

    console.log(`\n${header} (${lines.length})`);
    if (lines.length === 0) {
      console.log('  — no tasks —');
      continue;
    }
    // forEach: effects only, with no need to break out
    lines.forEach((line) => console.log(line));
  }

  // Summary footer: accumulators in a single walk (the 02-02 pattern)
  let open = 0;
  let openHours = 0;
  let overdue = 0;
  for (const task of tasks) {
    if (task.status === 'done') continue;
    open++;
    openHours += task.estimatedHours;
    if (task.dueDate < today) overdue++;
  }

  console.log(`\n───────────────────────────────────────`);
  console.log(`${open} open · ${openHours} h remaining · ${overdue} overdue`);
}

paintBoard(fullBacklog, TODAY);

Output (with the canonical backlog from 04-01):

═══ Taller Nómada board — 2026-09-20 ═══

UNDER WAY (2)
  1. ▸ [1] Redesign the multipurpose room · Iván · 12 h · #space #design
  2. ▸ [5] Bookbinding guide for residents · Iván · 8 h · #bookbinding #documentation

NOT STARTED (3)
  1. ○ [2] Signage for the screen-printing workshop · Marta · 6 h · #screen-printing #communication
  2. ○ [3] Update the bookings website · Lucía · 14 h · #web #bookings
  3. ○ [6] Carpentry workshop quote · Iván · 5 h · #carpentry #purchasing ⚠ OVERDUE

COMPLETED (1)
  1. ✓ [4] Screen-printing ink inventory · Marta · 3 h · #screen-printing #storeroom

───────────────────────────────────────
5 open · 45 h remaining · 1 overdue

Go back over the decisions taken, because they are the practical summary of the table in section 9:

Where Form chosen Why
Walking the statuses for...of with continue Fixed order, and blocks need skipping
Filtering tasks by status for...of with continue It needs continue to interrupt and a counter of its own
Turning tags into #tag map + join A pure transformation of one list into another
Printing the assembled lines forEach Effects only, nothing to break out of
Computing the summary for...of with accumulators Three totals in a single walk

In the next lesson you will see that that double filtering loop and that block of accumulators can be written in two lines with filter and reduce. But it is worth having done it by hand first: the 04-05 methods are not magic, they are these loops with a name.

Common Mistakes and Tips

1. Using for...in with arrays. String indexes and unexpected properties. Use for...of.

2. Trying to break inside forEach. It is a SyntaxError. Switch to for...of, or use find/some (04-05).

3. Believing that return inside forEach cuts the walk short. It only ends that pass; it behaves like continue.

4. Forgetting the return in a map callback. You get an array full of undefined. Remember: arrow with braces → return needed.

5. Using map for its side effects. If you are not going to use the array it returns, what you wanted was forEach:

backlog.map((t) => console.log(t.title));      // ✗ it creates an array of undefined nobody uses
backlog.forEach((t) => console.log(t.title));  // ✓

6. Mutating the array while walking it. Walk backwards, walk a copy, or —much better— build a new array.

7. Walking the same thing several times out of convenience. If you need three totals, get them in a single walk with three accumulators, not in three loops. With six tasks it is irrelevant; with twenty thousand it is not (Module 9).

Professional tip. Write the loop with whoever reads it six months from now in mind. for (const task of backlog) is understood without thinking; for (let i = 0; i < b.length; i++) forces you to check the bounds in your head. Save the classic for for the four cases where its extra control is indispensable: backwards, with a step, stopping before the end or comparing with the neighbor.

Exercises

Exercise 1 — Four walks. With the array const hours = [12, 6, 14, 3, 8, 5];:

  1. Print each value with its position using entries().
  2. Compute the total with a for...of.
  3. Use map to create an array inWorkdays with the hours divided by 8, rounded to one decimal.
  4. Print only the first three values, cutting the walk short with break.

Exercise 2 — First breach. Write firstOverloadedTask(tasks, limit) returning the first task whose estimatedHours exceed the limit, or null if there is none. It must stop walking as soon as it finds it. Explain why forEach is no good here. Try it with limit = 10 on the backlog.

Exercise 3 — Tag report by assignee. With the canonical backlog (fields assignee and tags), write linesByAssignee(tasks) returning an array of strings, one per task, in the format Iván · Redesign the multipurpose room · space, design. Then write printReport(tasks) that prints it numbered. Use map for the first and forEach with the index for the second, and justify the choice.

Solutions

Exercise 1

const hours = [12, 6, 14, 3, 8, 5];

// 1. entries() gives index and value, and would allow break
for (const pair of hours.entries()) {
  console.log(`Position ${pair[0]}: ${pair[1]} h`);
}

// 2. Accumulator with for...of
let total = 0;
for (const h of hours) total += h;
console.log(`Total: ${total} h`);        // Total: 48 h

// 3. map returns a new array of the same length
const inWorkdays = hours.map((h) => Number((h / 8).toFixed(1)));
console.log(inWorkdays);                 // [ 1.5, 0.8, 1.8, 0.4, 1, 0.6 ]

// 4. break needs a genuine loop: for...of, not forEach
for (const [i, h] of hours.entries()) {
  if (i === 3) break;
  console.log(h);                        // 12, 6, 14
}

The 48 h in point 2 are the backlog's canonical total. In point 3, toFixed returns a string, which is why we wrap it in Number(...): that is the explicit conversion from 01-07.

Exercise 2

function firstOverloadedTask(tasks, limit) {
  for (const task of tasks) {
    if (task.estimatedHours > limit) return task;   // leaves the loop and the function
  }
  return null;
}

const found = firstOverloadedTask(backlog, 10);
console.log(found?.title ?? 'none');            // 'Redesign the multipurpose room'
console.log(firstOverloadedTask(backlog, 100)); // null

forEach is no good for two reasons: it does not allow break, and a return inside the callback only ends that pass, so it would keep walking all six tasks even though the answer is in the first one. With for...of and return you leave the loop and the function in the same statement. In 04-05 you will see that this has a name of its own and takes a single line: tasks.find((t) => t.estimatedHours > limit).

Exercise 3

function linesByAssignee(tasks) {
  return tasks.map((task) =>
    `${task.assignee} · ${task.title} · ${task.tags.join(', ')}`
  );
}

function printReport(tasks) {
  const lines = linesByAssignee(tasks);
  lines.forEach((line, i) => console.log(`${i + 1}. ${line}`));
}

printReport(backlog);
// 1. Iván · Redesign the multipurpose room · space, design
// 2. Marta · Signage for the screen-printing workshop · screen-printing, communication
// 3. Lucía · Update the bookings website · web, bookings
// 4. Marta · Screen-printing ink inventory · screen-printing, storeroom
// 5. Iván · Bookbinding guide for residents · bookbinding, documentation
// 6. Iván · Carpentry workshop quote · carpentry, purchasing

The justification for the choices is the rule from section 9. linesByAssignee transforms a list of tasks into a list of strings of the same length: that is map by definition, and it also leaves the function pure and testable in Module 8. printReport only produces effects (printing) and needs the index for the numbering: forEach with its second argument fits exactly and there is nothing to interrupt. Separating the two —one function that builds the text and another that prints it— is the same separation between computing and displaying that you applied in 03-01 with describeTask.

Conclusion

You no longer walk arrays out of habit, but by decision. You know the six forms and their rules: the classic for when you need total control of the counter (backwards, with a step, stopping before the end or looking at the neighbor); for...of as the default option, because it removes the indexes, supports break and continue and works with any iterable; for...in reserved for objects, never for arrays, because it gives indexes as strings and drags in properties that are not elements; forEach for effects, with a convenient index but no way to interrupt; entries() when you need index and value without giving up break; and map when what you want is not to walk but to produce a new list of the same length.

The comparison table —does it give the index?, can it be broken out of?, what does it return?, does it wait for await?— settles the choice in seconds, and its last column leaves you forewarned about a problem you will solve in 05-06. You have also seen why forEach does not allow break (the loop is outside your function, as your own myForEach from 03-06 proved), how to walk arrays of objects and nested arrays without getting lost, and why mutating the list while walking it is an inexhaustible source of bugs, along with its three solutions. And you have printed the Taller Nómada board choosing the right form for each block, with the usual result: 5 open, 45 h remaining, 1 overdue.

But look at what that result cost. To group by status you wrote a loop inside another one with a continue. For the summary you needed three accumulator variables. To find the first overloaded task you wrote a whole function. And to sort the board by due date you still have no tool at all. All of those are operations so common that the language ships them ready-made and named: searching is find, checking is some and every, selecting is filter, sorting is sort and accumulating is reduce. That is the full arsenal of Searching, Sorting and Aggregating Data: find, sort and reduce, where JavaScript's most famous trap also awaits you: why [10, 9, 100].sort() does not return what you think.

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