The loops you wrote in lesson 02-02 have a flaw: they walk the whole list, always, even when the answer is known on the first pass. Checking whether there is an overdue task among forty and then carrying on through the remaining thirty-nine after finding it is wasted work. And there is something else you still cannot do: cross two lists —the three team members against the six tasks in the backlog— to build the board Marta wants to see. In this lesson you will learn to interrupt a loop with break, to skip iterations with continue, to nest loops and keep their cost under control, and to use labels for the rare cases where you need to leave several loops at once.

Contents

  1. break: leaving the loop as soon as you find what you are looking for
  2. The cost of walking too far
  3. continue: skipping one iteration
  4. break and continue in while and do...while
  5. break, continue or a flag: which to use
  6. Nested loops
  7. How the number of iterations grows
  8. break and continue inside nested loops
  9. Labels: break label and continue label
  10. Alternatives that read better than labels
  11. Case study: the assignee × status matrix
  12. Common Mistakes and Tips
  13. Exercises
  14. Conclusion

Every example starts from the Taller Nómada backlog you defined in lesson 02-02, extended with one more field:

const TODAY = '2026-09-20';

const ids        = [1, 2, 3, 4, 5, 6];
const titles     = [
  'Redesign the multipurpose room',
  'Signage for the screen-printing workshop',
  'Update the bookings website',
  'Screen-printing ink inventory',
  'Bookbinding guide for residents',
  'Carpentry workshop quote'
];
const assignees  = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
const priorities = ['high', 'medium', 'high', 'low', 'medium', 'high'];
const statuses   = ['in-progress', 'pending', 'pending', 'done', 'in-progress', 'pending'];
const hours      = [12, 6, 14, 3, 8, 5];
const dueDates   = ['2026-09-30', '2026-10-15', '2026-10-02', '2026-09-12', '2026-11-05', '2026-09-05'];
const archived   = [false, false, false, true, false, false];

  1. break: leaving the loop as soon as you find what you are looking for

break ends the loop immediately. Execution continues at the first statement after the loop: the rest of the body does not run, the update does not run and the condition is not checked again.

Marta wants to know which is the first high-priority task on the board so she can start there:

let foundId = 0;
let foundTitle = '';
let passes = 0;

for (let i = 0; i < ids.length; i++) {
  passes++;
  if (priorities[i] === 'high') {
    foundId = ids[i];
    foundTitle = titles[i];
    break;                       // ← that's it: no need to carry on
  }
}

console.log(`First high-priority one: [${foundId}] ${foundTitle}`);
console.log(`Iterations performed: ${passes} of ${ids.length}`);

Output:

First high-priority one: [1] Redesign the multipurpose room
Iterations performed: 1 of 6

One pass instead of six. The passes counter is there only to make the saving visible; you would not include it in real code.

Compare it with the version without break, which is the one you knew how to write until now:

// △ Correct, but it keeps searching after it has found it
for (let i = 0; i < ids.length; i++) {
  if (priorities[i] === 'high' && foundId === 0) {
    foundId = ids[i];
    foundTitle = titles[i];
  }
}

This version works, but it needs the extra condition foundId === 0 so that it does not end up with the last match instead of the first. break removes that artificial condition: it expresses the intention —"stop here"— instead of simulating it.

The project's second example, finding the first overdue task:

let hasOverdue = false;
let overdueTitle = '';

for (let i = 0; i < ids.length; i++) {
  if (dueDates[i] < TODAY && statuses[i] !== 'done') {
    hasOverdue = true;
    overdueTitle = titles[i];
    break;
  }
}

if (hasOverdue) {
  console.error(`⚠ There is at least one overdue task: ${overdueTitle}`);
} else {
  console.log('No overdue tasks.');
}
// ⚠ There is at least one overdue task: Carpentry workshop quote

Here the break saves nothing, because the only overdue task happens to be the last one in the array. With the forty tasks Marta will have next month, the saving will be real and variable. What it always provides is clarity: whoever reads the code immediately understands that we are looking for the first match and not for all of them.

  1. The cost of walking too far

With six tasks, walking too far is not noticeable: a computer does millions of comparisons per second. So why does it matter?

Because the cost grows with the data and with the work per iteration. These are the factors:

Factor Example in Nómada Tasks Impact
Number of elements 6 tasks today, 400 in two years Linear
Work per iteration Comparing two strings Negligible
Work per iteration Painting a row on screen High (Module 9)
Nested loops 3 assignees × 400 tasks Multiplicative
Side effects Sending an alert for each match It can duplicate alerts

The last factor is the most important one and has nothing to do with speed: if the loop body does something —sending an email, incrementing a global counter, writing to the screen—, carrying on after finding the answer is not just slow, it is incorrect. Without break, the search for the first overdue task would send an alert for every overdue task, and Marta would get five emails instead of one.

Rule: when the question is "is there any?" or "which is the first?", get out with break. When the question is "how many?" or "what is the total?", you have to walk the whole thing and no break will do.

  1. continue: skipping one iteration

continue skips the rest of the body and moves on to the next iteration. It does not leave the loop: in a for, it runs the update and checks the condition again.

Its most common use is discarding elements you do not care about at the top of the body, leaving the rest of the code unindented. It is the guard clause from lesson 02-01, this time with a real exit:

let activeHours = 0;

for (let i = 0; i < ids.length; i++) {
  if (archived[i]) continue;            // archived ones do not count
  if (statuses[i] === 'done') continue; // neither do finished ones

  activeHours += hours[i];
  console.log(`[${ids[i]}] ${titles[i]} · ${hours[i]} h`);
}

console.log(`Active hours on the board: ${activeHours} h`);

Output:

[1] Redesign the multipurpose room · 12 h
[2] Signage for the screen-printing workshop · 6 h
[3] Update the bookings website · 14 h
[5] Bookbinding guide for residents · 8 h
[6] Carpentry workshop quote · 5 h
Active hours on the board: 45 h

Compare that structure with the equivalent one using a wrapping if:

// △ Same result, one more level of indentation
for (let i = 0; i < ids.length; i++) {
  if (!archived[i] && statuses[i] !== 'done') {
    activeHours += hours[i];
    console.log(`[${ids[i]}] ${titles[i]} · ${hours[i]} h`);
  }
}

With two exclusions both versions are readable. With four or five, the wrapping condition turns into an endless line full of negations and &&, while the continue guards remain a list of reasons for discarding, each on its own line and with its own comment. That is the real advantage of continue: it keeps the body flat and separates the exclusions from the main logic.

Single-line continue statements are the one reasonable exception to the "always use braces" rule from lesson 02-01: if (archived[i]) continue; fits on one line, cannot grow accidentally and reads like a sentence.

  1. break and continue in while and do...while

They work the same way in all three loops, with one dangerous difference in while: continue jumps back to the start of the loop without running any update, because in a while the update is something you write yourself in the body.

// ✗ INFINITE LOOP
let i = 0;
while (i < ids.length) {
  if (archived[i]) continue;   // ← i is never incremented: it stays at 3 forever
  console.log(titles[i]);
  i++;
}

When it reaches i = 3 (the archived task), continue jumps straight to checking the condition without running the i++, so i stays at 3 forever. The correct version increments before the guard:

let i = -1;
while (i < ids.length - 1) {
  i++;
  if (archived[i]) continue;
  console.log(titles[i]);
}

The result is correct but awkward to read, and that is why there is a simple rule: if you need continue, use a for, where the update lives in the header and always runs.

There is another trap that connects with the previous lesson: a break inside a switch leaves the switch, not the loop.

for (let i = 0; i < ids.length; i++) {
  switch (statuses[i]) {
    case 'done':
      break;        // ← leaves the switch; the loop continues with the next task
  }
  console.log(titles[i]);   // this line also runs for the finished ones
}

If what you want is to abandon the loop from inside a switch, you need a flag or a label, which you will see in section 9.

  1. break, continue or a flag: which to use

Tool What it does Use it when
break Ends the loop You are looking for the first match or checking whether any exists
continue Jumps to the next iteration You want to discard elements and keep the body flat
Boolean flag Remembers that something happened You need to keep walking and remember the find
Condition in the header Stops the loop cleanly The stopping condition is simple and natural (while (!found && i < n))

That last row deserves an example, because it is the elegant alternative to break when the loop is a while:

let index = 0;
let found = false;

while (!found && index < ids.length) {
  if (dueDates[index] < TODAY && statuses[index] !== 'done') {
    found = true;
  } else {
    index++;
  }
}

console.log(found ? `Overdue: ${titles[index]}` : 'None overdue');

The condition in the header declares all the reasons why the loop can end, without anyone having to hunt for a break hidden twenty lines below. With long loops it reads better; with short loops, break wins on simplicity. Both are correct.

  1. Nested loops

A nested loop is a loop inside the body of another one. The inner one runs completely on every iteration of the outer one.

The project's case: Marta wants the split of tasks per person without writing one counter per team member, as you did in lesson 02-02.

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

for (let p = 0; p < team.length; p++) {            // outer loop: people
  let count = 0;
  let personHours = 0;

  for (let t = 0; t < ids.length; t++) {           // inner loop: tasks
    if (assignees[t] !== team[p]) continue;
    if (statuses[t] === 'done') continue;

    count++;
    personHours += hours[t];
  }

  console.log(`${team[p].padEnd(6)} ${count} open task(s) · ${personHours} h`);
}

Output:

Marta  1 open task(s) · 6 h
Iván   3 open task(s) · 25 h
Lucía  1 open task(s) · 14 h

The same report as in lesson 02-02, but with no three repeated variables and no else if chain: if someone new joins the team tomorrow, you add their name to the team array and the code does not change. That is the benefit of nesting: it turns a repetition written by hand into a repetition that is generated.

Notice two details:

  • The count and personHours counters are declared inside the outer loop. They have to be reset for each person. If they were fully outside, they would accumulate the totals of the whole team.
  • Each loop has its own control variable, p and t. Reusing i in both is the classic nested-loop bug: the inner one leaves it at its final value and the outer one finishes early.
flowchart TD
    A["p = 0 · Marta"] --> B["t = 0..5<br/>walks the 6 tasks"]
    B --> C["Prints Marta's row"]
    C --> D["p = 1 · Iván"]
    D --> E["t = 0..5<br/>walks the 6 tasks"]
    E --> F["Prints Iván's row"]
    F --> G["p = 2 · Lucía"]
    G --> H["t = 0..5<br/>walks the 6 tasks"]
    H --> I["Prints Lucía's row"]
    I --> J["End: 3 × 6 = 18 inner iterations"]

  1. How the number of iterations grows

The inner loop runs once in full for every pass of the outer one. The total number of inner iterations is the product of both sizes.

People Tasks Inner iterations
3 6 18
3 400 1,200
10 400 4,000
400 400 160,000

The first three rows are irrelevant to a computer. The last one illustrates the danger: when both loops walk the same list of data, the cost grows with the square of the size. Doubling the data quadruples the work. That is called quadratic cost and it is the most frequent reason for an interface freezing up; you will study it in detail in Optimizing JavaScript Performance.

Two practical rules for the level you are at:

  1. Nesting a small loop (the team: 3) with a big one (the tasks: 400) is perfectly acceptable. The cost is 3 × 400, that is, linear with a constant factor.
  2. Nesting two loops over the same large list is the warning sign. Comparing each task with all the others —to detect duplicate titles, for example— is exactly that case. There is usually a solution with a single pass and a lookup object; you will see it in Module 4.

A third level of nesting (people × statuses × tasks) almost always means the problem can be reformulated. If you find yourself writing the third for, stop and think.

  1. break and continue inside nested loops

Fundamental rule: break and continue only affect the innermost loop that contains them.

for (let p = 0; p < team.length; p++) {
  for (let t = 0; t < ids.length; t++) {
    if (assignees[t] === team[p] && dueDates[t] < TODAY && statuses[t] !== 'done') {
      console.log(`${team[p]} has an overdue task: ${titles[t]}`);
      break;      // ← leaves the TASKS loop, not the PEOPLE loop
    }
  }
}

Output:

Iván has an overdue task: Carpentry workshop quote

The break cuts the search short within the current person —one overdue task per person is enough for the alert— and the outer loop carries on with the next one. That is exactly the behavior we want here, and in the vast majority of real cases.

The problem shows up when you want to abandon both loops at once: for instance, stopping as soon as you find the first person with an overdue task, without reviewing the rest of the team. A plain break cannot do it. The traditional solution is a flag checked at both levels:

let culprit = '';

for (let p = 0; p < team.length && culprit === ''; p++) {
  for (let t = 0; t < ids.length; t++) {
    if (assignees[t] === team[p] && dueDates[t] < TODAY && statuses[t] !== 'done') {
      culprit = team[p];
      break;
    }
  }
}

console.log(culprit !== '' ? `First alert for ${culprit}` : 'Everything on schedule');
// First alert for Iván

The extra condition && culprit === '' in the header of the outer loop does the job. It works, but you have to remember to write it, and with three levels of nesting it gets awkward. That is what labels are for.

  1. Labels: break label and continue label

A label is a name followed by a colon placed just before a loop. It lets break and continue state which loop they refer to.

let culprit = '';

search:                                            // ← the label
for (let p = 0; p < team.length; p++) {
  for (let t = 0; t < ids.length; t++) {
    if (assignees[t] === team[p] && dueDates[t] < TODAY && statuses[t] !== 'done') {
      culprit = team[p];
      break search;                                // ← leaves BOTH loops
    }
  }
}

console.log(`First alert for ${culprit}`);         // First alert for Iván

break search; ends the loop labeled search, that is, the outer one, and everything it contains along with it. Execution continues at the console.log.

continue label is less frequent but it exists too: it jumps to the next iteration of the labeled loop.

personReview:
for (let p = 0; p < team.length; p++) {
  for (let t = 0; t < ids.length; t++) {
    if (assignees[t] !== team[p]) continue;

    if (hours[t] > 40) {
      console.error(`${team[p]}: task that violates R3. This person is skipped.`);
      continue personReview;     // ← abandons this person, moves on to the next
    }
    console.log(`${team[p]} · ${titles[t]}`);
  }
}

Points of syntax and use:

  • The label goes immediately before the loop, usually on its own line and unindented.
  • The name follows the identifier rules from lesson 01-04, and by convention it is written in lowercase or camelCase describing the purpose: search, personReview.
  • break label and continue label can only be used inside the labeled loop.
  • Technically a label can mark any block, not only a loop, but that is extremely rare and you will not need it.

Why they are seldom used. Labels are correct, legal code, but many teams ban them in their style guide, for two reasons:

  1. They are cousins of goto, and jumping to a marked point from inside several blocks breaks the sequential reading of the code.
  2. There is almost always a better alternative, and that alternative is usually extracting the inner loop into a function and leaving with return —you will see it in Defining and Calling Functions—.

When they are the clean solution: when you have two or three nested loops, the early exit affects all of them and extracting a function would complicate the code more than it simplifies it. In that specific situation, break label is clearer than three flags checked in three headers.

  1. Alternatives that read better than labels

Before reaching for a label, consider these three options in order:

1. A flag in the outer loop's condition. The one from section 8. Advantage: the stopping condition is visible in the header. Drawback: you have to remember to write it in two places.

2. Extracting the logic into a function and using return. The option you will use from Module 3 onward. return leaves the entire function, cutting through all the loops at once, and it also gives the operation a name:

// A preview of Module 3 — you do not need to understand it yet
function findFirstCulprit(team, assignees, dueDates, statuses, today) {
  for (let p = 0; p < team.length; p++) {
    for (let t = 0; t < assignees.length; t++) {
      if (assignees[t] === team[p] && dueDates[t] < today && statuses[t] !== 'done') {
        return team[p];      // leaves both loops and the function
      }
    }
  }
  return null;
}

Compare this with the labeled version: the name findFirstCulprit documents the intention, the result can be reused and there is no label to interpret. This is the underlying reason why labels are so rarely seen in modern code.

3. Reformulating so that you do not need the nesting. Very often the second loop is unnecessary. The culprit example can be solved with a single pass over the tasks, because each task already knows who its assignee is:

let culprit = '';

for (let t = 0; t < ids.length; t++) {
  if (dueDates[t] < TODAY && statuses[t] !== 'done') {
    culprit = assignees[t];
    break;
  }
}

console.log(`First alert for ${culprit}`);   // First alert for Iván

Six iterations in the worst case instead of eighteen, a single break with no label and less code. When a nested loop forces you to use labels, suspect the nesting first. Very often the problem can be walked in a single pass.

  1. Case study: the assignee × status matrix

The Nómada Tasks board needs a cross-table: how many tasks each person has in each status. It is the canonical nested-loop example, this time crossing two configuration lists —team and STATUSES— against the data list.

const team = ['Marta', 'Iván', 'Lucía'];
const STATUSES = ['pending', 'in-progress', 'done'];

console.log('Person    pending     in-progress done        TOTAL');
console.log('---------------------------------------------------');

const statusTotals = [0, 0, 0];

for (let p = 0; p < team.length; p++) {
  let row = team[p].padEnd(10);
  let personTotal = 0;

  for (let e = 0; e < STATUSES.length; e++) {
    let count = 0;

    for (let t = 0; t < ids.length; t++) {
      if (archived[t]) continue;
      if (assignees[t] !== team[p]) continue;
      if (statuses[t] !== STATUSES[e]) continue;
      count++;
    }

    row += String(count).padEnd(12);
    personTotal += count;
    statusTotals[e] += count;
  }

  console.log(row + personTotal);
}

console.log('---------------------------------------------------');
console.log(
  'TOTAL     ' +
  String(statusTotals[0]).padEnd(12) +
  String(statusTotals[1]).padEnd(12) +
  String(statusTotals[2]).padEnd(12) +
  (statusTotals[0] + statusTotals[1] + statusTotals[2])
);

Output:

Person    pending     in-progress done        TOTAL
---------------------------------------------------
Marta     1           0           0           1
Iván      1           2           0           3
Lucía     1           0           0           1
---------------------------------------------------
TOTAL     3           2           0           5

What is going on here, layer by layer:

  • Three levels of nesting: 3 people × 3 statuses × 6 tasks = 54 inner iterations. With three people and three fixed statuses, those factors are constant, so the real cost grows only with the number of tasks.
  • The three continue guards discard whatever does not belong in the current cell. Written as a single condition they would be !archived[t] && assignees[t] === team[p] && statuses[t] === STATUSES[e], correct but far less readable.
  • Task 4 (Screen-printing ink inventory) does not appear in any cell because it is archived: the first guard excludes it. That is why the done column is zero and the total is 5 and not 6.
  • statusTotals is an accumulator array: it is declared before everything and incremented by status index, adding up the columns as they are calculated.
  • row is built by concatenation inside the statuses loop and printed once per person. Accumulating text in a variable is the same accumulator pattern from lesson 02-02, with '' as the neutral value.

This third level of nesting is exactly what section 7 advised against, and there is a fix: walking the tasks a single time and adding to the matching cell. But that requires indexing by two keys at once, which is precisely what the objects in Module 4 do. For now, three loops over six tasks is a correct and perfectly acceptable solution.

Common Mistakes and Tips

Believing that break leaves every loop. It only leaves the innermost one. To leave several, use a label or a flag.

Using break inside a switch expecting to leave the loop. That break belongs to the switch. It is a silent bug and very hard to spot.

continue in a while without having updated the counter. A guaranteed infinite loop. If you need continue, use for.

Reusing the control variable in nested loops.

// ✗ The inner loop destroys the outer one's index
for (let i = 0; i < team.length; i++) {
  for (let i = 0; i < ids.length; i++) { }   // it shadows the outer one; confusing behavior
}

Use different names and, better still, descriptive ones: p for people, t for tasks, e for statuses.

Declaring the accumulator at the wrong level. In a nested loop, the position of the declaration decides the meaning: outside everything it is the global total, inside the outer loop it is the total per person, inside the inner loop it accumulates nothing. It is the number-one source of strange results in cross-tables.

Putting code after a break in the same block. It never runs. Some editors gray it out; if you see gray code, there is dead logic.

Overusing continue. Three guards at the top of the body are excellent; eight continue statements spread across fifty lines turn the loop into a maze. If you get to that point, the loop needs splitting into functions.

Tip: put the break as close as possible to the condition that justifies it. A break at the end of a long body is hard to connect with its cause.

Tip: comment why you are leaving, not that you are leaving. break; // we have already found it adds nothing; break; // one alert per person is enough does.

Tip: to debug a nested loop, print both indexes together. console.log(p, t, team[p], titles[t]) as the first line of the inner body shows you the full traversal and instantly reveals whether a counter is at the wrong level.

Exercises

Exercise 1 — First task available for someone

Write a loop that finds the first pending, non-archived, not-yet-overdue task that Lucía could start, since her current workload is the lowest on the team. Use continue for the exclusions and break to stop as soon as you find it. Display its id, its title and the hours. If there is none, say so.

Exercise 2 — Balanced workload

Walk the team with a nested loop and calculate, for each person, their open hours (status other than 'done' and not archived). Then work out who has the most hours and who has the fewest, and suggest moving a task. Be careful about where you declare each accumulator.

Exercise 3 — Labels versus reformulation

The workshop does not allow two open tasks with the same priority and the same assignee falling due in the same month. Write a nested loop that compares each task with the ones after it and stops at the first conflict, using break label. Then answer: how many comparisons does this algorithm make with 6 tasks? And with 400?

Solutions

Exercise 1

let availableId = 0;
let availableTitle = '';
let availableHours = 0;

for (let t = 0; t < ids.length; t++) {
  if (archived[t]) continue;                    // off the board
  if (statuses[t] !== 'pending') continue;      // already started or finished
  if (dueDates[t] < TODAY) continue;            // overdue: Marta reassigns it

  availableId = ids[t];
  availableTitle = titles[t];
  availableHours = hours[t];
  break;                                        // the first one is enough
}

if (availableId === 0) {
  console.log('There is no task available to assign to Lucía.');
} else {
  console.log(`Suggestion for Lucía: [${availableId}] ${availableTitle} (${availableHours} h)`);
}
// Suggestion for Lucía: [2] Signage for the screen-printing workshop (6 h)

The traversal discards task 1 (it is in-progress) and settles on task 2, which is pending, is not archived and is due on 2026-10-15, still ahead of TODAY. Two iterations out of six.

The three guards are ordered from the cheapest and most exclusive to the least: first a straight boolean, then a string comparison, and finally the date one. It does not change the result, but it is a good habit: when the checks become expensive, the order will matter.

availableId = 0 works as a sentinel because rule R1 guarantees that identifiers are positive integers: 0 cannot be a real id.

Exercise 2

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

let maxHours = -1;
let maxPerson = '';
let minHours = Infinity;
let minPerson = '';

for (let p = 0; p < team.length; p++) {
  let personHours = 0;                     // ← it resets for each person

  for (let t = 0; t < ids.length; t++) {
    if (archived[t]) continue;
    if (statuses[t] === 'done') continue;
    if (assignees[t] !== team[p]) continue;
    personHours += hours[t];
  }

  console.log(`${team[p].padEnd(6)} ${personHours} h open`);

  if (personHours > maxHours) {
    maxHours = personHours;
    maxPerson = team[p];
  }
  if (personHours < minHours) {
    minHours = personHours;
    minPerson = team[p];
  }
}

console.log(`\nMost loaded:  ${maxPerson} (${maxHours} h)`);
console.log(`Least loaded: ${minPerson} (${minHours} h)`);
console.log(`Imbalance: ${maxHours - minHours} h`);

if (maxHours - minHours > 10) {
  console.warn(`Marta should move a task from ${maxPerson} to ${minPerson}.`);
}

Output:

Marta  6 h open
Iván   25 h open
Lucía  14 h open

Most loaded:  Iván (25 h)
Least loaded: Marta (6 h)
Imbalance: 19 h

What matters is where each variable lives:

Variable Where it is declared Why
personHours Inside the outer loop It has to start at 0 for each person
maxHours, maxPerson Outside both loops They have to survive every person

If personHours were declared outside, it would accumulate the team total and each row would show a bigger number than the previous one: 6, 31, 45. It is a bug that produces plausible numbers, which is why it is hard to spot. If maxHours were declared inside, it would reset for each person and the final result would always be the last one's.

Infinity as the initial value of minHours is the ideal sentinel for a minimum: every real number is smaller, so the first person always replaces it. You met it in Variables and Data Types.

Exercise 3

let conflict = false;
let taskA = 0;
let taskB = 0;

comparison:
for (let a = 0; a < ids.length; a++) {
  if (statuses[a] === 'done' || archived[a]) continue;

  for (let b = a + 1; b < ids.length; b++) {          // ← b starts at a + 1
    if (statuses[b] === 'done' || archived[b]) continue;

    const samePriority = priorities[a] === priorities[b];
    const sameAssignee = assignees[a] === assignees[b];
    const sameMonth = dueDates[a].slice(0, 7) === dueDates[b].slice(0, 7);

    if (samePriority && sameAssignee && sameMonth) {
      conflict = true;
      taskA = ids[a];
      taskB = ids[b];
      break comparison;                               // ← leaves both loops
    }
  }
}

if (conflict) {
  console.error(`Scheduling conflict between tasks ${taskA} and ${taskB}.`);
} else {
  console.log('There are no scheduling conflicts.');
}
// Scheduling conflict between tasks 1 and 6.

dueDates[a].slice(0, 7) keeps the first seven characters of the ISO date, that is, '2026-09': year and month. It is the simplest way to compare "same month" when the dates are text in ISO format.

The backlog does have a conflict: task 1 (Redesign the multipurpose room, Iván, high priority, due 2026-09-30) and task 6 (Carpentry workshop quote, Iván, high priority, due 2026-09-05) share assignee, priority and month. The algorithm detects it when comparing the pair a = 0, b = 5, and the label cuts both loops short right there.

This result illustrates something valuable: applying a new business rule to real data usually turns up conflicts nobody had noticed. Marta would have to decide whether to move one of the two tasks to October or to relax the rule.

The key detail of the algorithm is let b = a + 1. If b started at 0, each pair would be compared twice (A with B and B with A) and, worse still, each task would be compared with itself, always producing a false conflict. Starting at a + 1 compares each pair exactly once.

Number of comparisons: with n tasks, the loop makes n × (n - 1) / 2 comparisons.

Tasks Comparisons
6 15
40 780
400 79,800

Multiplying the number of tasks by 10 multiplies the work by 100: it is the quadratic cost from section 7 in its purest form. With 400 tasks and such a simple calculation it is still instantaneous, but if the loop had to paint something or request data from a server, the application would grind to a halt. The solution —grouping the tasks by the key assignee + priority + month in a single pass— arrives in Module 4.

And notice that in this particular case the label is justified: the early exit affects two loops, a flag would have to be checked in both headers and you do not have functions yet to extract the search. It is exactly the niche of break label.

Conclusion

You now control the flow inside a loop precisely. You know that break ends the loop on the spot and that it is the right tool for "the first match" and for "is there any?", not only for speed but because carrying on after finding the answer can duplicate side effects. You know that continue skips to the next iteration and that its best use is guards at the top of the body, which keep the code flat and separate the exclusions from the main logic. And you know its two traps: continue in a while without updating the counter causes an infinite loop, and a break inside a switch leaves the switch, not the loop.

You know how to nest loops to cross two lists —the team against the backlog—, to place each accumulator at the right level and to estimate the cost: acceptable when one of the loops is small and fixed, dangerous when both walk the same large list. You know about labels, break label and continue label, when they are the clean solution and why in most cases there is something better: a flag in the header, a single well-designed pass or, from Module 3 onward, a function with return.

Applied to Nómada Tasks, you have already built the board's assignee × status matrix, detected the workload imbalance between Iván and Marta and written the first scheduling rule that compares tasks with one another.

All of this assumes one thing: that the data is correct. But what happens when the title arrives empty, when estimatedHours is the text 'twelve' or when someone tries to read a field of a task that does not exist? Until now your code would have produced a silent NaN or stopped with an incomprehensible error. In Error Handling with try-catch, the last lesson of the module, you will learn to detect those situations, to throw errors with useful messages and to decide what to do when something goes wrong, without the whole application collapsing.

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