Los bucles que escribiste en la lección 02-02 tienen un defecto: recorren la lista entera, siempre, aunque la respuesta se conozca en la primera vuelta. Buscar si hay alguna tarea vencida entre cuarenta y seguir comprobando las treinta y nueve restantes después de encontrarla es trabajo tirado. Y hay otra cosa que todavía no sabes hacer: cruzar dos listas —los tres miembros del equipo contra las seis tareas del backlog— para construir el tablero que Marta quiere ver. En esta lección aprenderás a interrumpir un bucle con break, a saltarte iteraciones con continue, a anidar bucles y controlar su coste, y a usar etiquetas para los casos raros en los que hace falta salir de varios bucles a la vez.

Contenido

  1. break: salir del bucle en cuanto encuentras lo que buscas
  2. El coste de recorrer de más
  3. continue: saltarse una iteración
  4. break y continue en while y do...while
  5. break, continue o bandera: cuál usar
  6. Bucles anidados
  7. El crecimiento del número de iteraciones
  8. break y continue dentro de bucles anidados
  9. Etiquetas: break etiqueta y continue etiqueta
  10. Alternativas más legibles que las etiquetas
  11. Caso práctico: la matriz responsable × estado
  12. Errores Comunes y Consejos
  13. Ejercicios
  14. Conclusión

Todos los ejemplos parten del backlog de Taller Nómada que definiste en la lección 02-02, ampliado con un campo más:

const HOY = '2026-09-20';

const ids          = [1, 2, 3, 4, 5, 6];
const titulos      = [
  'Rediseñar la sala polivalente',
  'Cartelería del taller de serigrafía',
  'Actualizar la web de reservas',
  'Inventario de tintas de serigrafía',
  'Guía de encuadernación para residentes',
  'Presupuesto de la carpintería'
];
const responsables = ['Iván', 'Marta', 'Lucía', 'Marta', 'Iván', 'Iván'];
const prioridades  = ['alta', 'media', 'alta', 'baja', 'media', 'alta'];
const estados      = ['en-curso', 'pendiente', 'pendiente', 'hecha', 'en-curso', 'pendiente'];
const horas        = [12, 6, 14, 3, 8, 5];
const fechasLimite = ['2026-09-30', '2026-10-15', '2026-10-02', '2026-09-12', '2026-11-05', '2026-09-05'];
const archivadas   = [false, false, false, true, false, false];

  1. break: salir del bucle en cuanto encuentras lo que buscas

break termina el bucle inmediatamente. La ejecución continúa en la primera sentencia posterior al bucle: no se ejecuta el resto del cuerpo, no se ejecuta la actualización y no se vuelve a comprobar la condición.

Marta quiere saber cuál es la primera tarea de prioridad alta del tablero para empezar por ahí:

let idEncontrada = 0;
let tituloEncontrado = '';
let vueltas = 0;

for (let i = 0; i < ids.length; i++) {
  vueltas++;
  if (prioridades[i] === 'alta') {
    idEncontrada = ids[i];
    tituloEncontrado = titulos[i];
    break;                       // ← ya está: no hace falta seguir
  }
}

console.log(`Primera prioritaria: [${idEncontrada}] ${tituloEncontrado}`);
console.log(`Iteraciones realizadas: ${vueltas} de ${ids.length}`);

Salida:

Primera prioritaria: [1] Rediseñar la sala polivalente
Iteraciones realizadas: 1 de 6

Una vuelta en lugar de seis. El contador vueltas está solo para hacer visible el ahorro; en código real no se pone.

Compáralo con la versión sin break, que es la que sabías escribir hasta ahora:

// △ Correcta, pero sigue buscando después de encontrar
for (let i = 0; i < ids.length; i++) {
  if (prioridades[i] === 'alta' && idEncontrada === 0) {
    idEncontrada = ids[i];
    tituloEncontrado = titulos[i];
  }
}

Esta versión funciona, pero necesita la condición extra idEncontrada === 0 para no quedarse con la última coincidencia en lugar de la primera. break elimina esa condición artificial: expresa la intención —"para aquí"— en lugar de simularla.

El segundo ejemplo del proyecto, buscar la primera tarea vencida:

let hayVencida = false;
let tituloVencida = '';

for (let i = 0; i < ids.length; i++) {
  if (fechasLimite[i] < HOY && estados[i] !== 'hecha') {
    hayVencida = true;
    tituloVencida = titulos[i];
    break;
  }
}

if (hayVencida) {
  console.error(`⚠ Hay al menos una tarea vencida: ${tituloVencida}`);
} else {
  console.log('Ninguna tarea vencida.');
}
// ⚠ Hay al menos una tarea vencida: Presupuesto de la carpintería

Aquí el break no ahorra nada, porque la única vencida es justo la última del array. Con las cuarenta tareas que Marta tendrá el mes que viene, el ahorro será real y variable. Lo que sí aporta siempre es claridad: quien lee el código entiende de inmediato que se busca la primera coincidencia y no todas.

  1. El coste de recorrer de más

Con seis tareas, recorrer de más no se nota: un ordenador hace millones de comparaciones por segundo. Entonces, ¿por qué importa?

Porque el coste crece con los datos y con el trabajo por iteración. Estos son los factores:

Factor Ejemplo en Nómada Tareas Impacto
Número de elementos 6 tareas hoy, 400 en dos años Lineal
Trabajo por iteración Comparar dos textos Despreciable
Trabajo por iteración Pintar una fila en la pantalla Alto (Módulo 9)
Bucles anidados 3 responsables × 400 tareas Multiplicativo
Efectos secundarios Enviar un aviso por cada coincidencia Puede duplicar avisos

El último factor es el más importante y no tiene nada que ver con la velocidad: si el cuerpo del bucle hace algo —enviar un correo, incrementar un contador global, escribir en pantalla—, seguir iterando después de encontrar la respuesta no es solo lento, es incorrecto. Sin break, la búsqueda de la primera tarea vencida enviaría un aviso por cada tarea vencida, y Marta recibiría cinco correos en lugar de uno.

Norma: cuando la pregunta es "¿existe alguna?" o "¿cuál es la primera?", sal con break. Cuando la pregunta es "¿cuántas?" o "¿cuál es el total?", hay que recorrerlo todo y no hay break que valga.

  1. continue: saltarse una iteración

continue salta el resto del cuerpo y pasa a la siguiente iteración. No sale del bucle: en un for, ejecuta la actualización y vuelve a comprobar la condición.

Su uso más habitual es descartar elementos que no interesan al principio del cuerpo, dejando el resto del código sin indentar. Es la cláusula de guarda de la lección 02-01, ahora sí con una salida de verdad:

let horasActivas = 0;

for (let i = 0; i < ids.length; i++) {
  if (archivadas[i]) continue;            // las archivadas no cuentan
  if (estados[i] === 'hecha') continue;   // las hechas tampoco

  horasActivas += horas[i];
  console.log(`[${ids[i]}] ${titulos[i]} · ${horas[i]} h`);
}

console.log(`Horas activas en el tablero: ${horasActivas} h`);

Salida:

[1] Rediseñar la sala polivalente · 12 h
[2] Cartelería del taller de serigrafía · 6 h
[3] Actualizar la web de reservas · 14 h
[5] Guía de encuadernación para residentes · 8 h
[6] Presupuesto de la carpintería · 5 h
Horas activas en el tablero: 45 h

Compara esa estructura con la equivalente usando un if envolvente:

// △ Mismo resultado, un nivel más de indentación
for (let i = 0; i < ids.length; i++) {
  if (!archivadas[i] && estados[i] !== 'hecha') {
    horasActivas += horas[i];
    console.log(`[${ids[i]}] ${titulos[i]} · ${horas[i]} h`);
  }
}

Con dos exclusiones ambas versiones son legibles. Con cuatro o cinco, la condición envolvente se convierte en una línea kilométrica llena de negaciones y &&, mientras que las guardas con continue siguen siendo una lista de motivos de descarte, cada uno en su línea y con su comentario. Esa es la ventaja real de continue: mantiene el cuerpo plano y separa los descartes de la lógica principal.

Los continue de una sola línea son la única excepción razonable a la norma de "llaves siempre" de la lección 02-01: if (archivadas[i]) continue; cabe en una línea, no puede crecer accidentalmente y se lee como una frase.

  1. break y continue en while y do...while

Funcionan igual en los tres bucles, con una diferencia peligrosa en while: continue salta al inicio del bucle sin ejecutar ninguna actualización, porque en un while la actualización la escribes tú en el cuerpo.

// ✗ BUCLE INFINITO
let i = 0;
while (i < ids.length) {
  if (archivadas[i]) continue;   // ← i nunca se incrementa: se queda en la 3 para siempre
  console.log(titulos[i]);
  i++;
}

Al llegar a i = 3 (la tarea archivada), continue salta directamente a comprobar la condición sin ejecutar el i++, así que i sigue valiendo 3 eternamente. La versión correcta incrementa antes de la guarda:

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

El resultado es correcto pero incómodo de leer, y por eso hay una norma sencilla: si necesitas continue, usa un for, donde la actualización va en la cabecera y siempre se ejecuta.

Hay otra trampa que conecta con la lección anterior: break dentro de un switch sale del switch, no del bucle.

for (let i = 0; i < ids.length; i++) {
  switch (estados[i]) {
    case 'hecha':
      break;        // ← sale del switch; el bucle sigue con la tarea siguiente
  }
  console.log(titulos[i]);   // esta línea se ejecuta también para las hechas
}

Si lo que quieres es abandonar el bucle desde dentro de un switch, necesitas una bandera o una etiqueta, que verás en la sección 9.

  1. break, continue o bandera: cuál usar

Herramienta Qué hace Úsala cuando
break Termina el bucle Buscas la primera coincidencia o compruebas si existe alguna
continue Salta a la siguiente iteración Quieres descartar elementos y mantener el cuerpo plano
Bandera booleana Recuerda que algo ocurrió Necesitas seguir recorriendo y recordar el hallazgo
Condición en la cabecera Detiene el bucle limpiamente La condición de parada es simple y natural (while (!encontrado && i < n))

Esa última fila merece un ejemplo, porque es la alternativa elegante a break cuando el bucle es un while:

let indice = 0;
let encontrada = false;

while (!encontrada && indice < ids.length) {
  if (fechasLimite[indice] < HOY && estados[indice] !== 'hecha') {
    encontrada = true;
  } else {
    indice++;
  }
}

console.log(encontrada ? `Vencida: ${titulos[indice]}` : 'Ninguna vencida');

La condición de la cabecera declara todas las razones por las que el bucle puede terminar, sin que haya que buscar un break escondido veinte líneas más abajo. Con bucles largos es más legible; con bucles cortos, break gana por simplicidad. Ambas son correctas.

  1. Bucles anidados

Un bucle anidado es un bucle dentro del cuerpo de otro. El interno se ejecuta completo en cada iteración del externo.

El caso del proyecto: Marta quiere el reparto de tareas por persona sin escribir un contador por cada miembro del equipo, como hiciste en la lección 02-02.

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

for (let p = 0; p < equipo.length; p++) {          // bucle externo: personas
  let cuantas = 0;
  let horasPersona = 0;

  for (let t = 0; t < ids.length; t++) {           // bucle interno: tareas
    if (responsables[t] !== equipo[p]) continue;
    if (estados[t] === 'hecha') continue;

    cuantas++;
    horasPersona += horas[t];
  }

  console.log(`${equipo[p].padEnd(6)} ${cuantas} tarea(s) abiertas · ${horasPersona} h`);
}

Salida:

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

El mismo informe de la lección 02-02, pero sin tres variables repetidas y sin cadena de else if: si mañana entra alguien nuevo al equipo, se añade su nombre al array equipo y el código no cambia. Ese es el beneficio del anidamiento: convierte una repetición escrita a mano en una repetición generada.

Fíjate en dos detalles:

  • Los contadores cuantas y horasPersona se declaran dentro del bucle externo. Deben reiniciarse en cada persona. Si estuvieran fuera del todo, acumularían los totales del equipo entero.
  • Cada bucle tiene su propia variable de control, p y t. Reutilizar i en ambos es el error clásico de los bucles anidados: el interno la deja con el valor final y el externo termina antes de tiempo.
flowchart TD
    A["p = 0 · Marta"] --> B["t = 0..5<br/>recorre las 6 tareas"]
    B --> C["Imprime la fila de Marta"]
    C --> D["p = 1 · Iván"]
    D --> E["t = 0..5<br/>recorre las 6 tareas"]
    E --> F["Imprime la fila de Iván"]
    F --> G["p = 2 · Lucía"]
    G --> H["t = 0..5<br/>recorre las 6 tareas"]
    H --> I["Imprime la fila de Lucía"]
    I --> J["Fin: 3 × 6 = 18 iteraciones internas"]

  1. El crecimiento del número de iteraciones

El bucle interno se ejecuta una vez completo por cada vuelta del externo. El total de iteraciones internas es el producto de ambos tamaños.

Personas Tareas Iteraciones internas
3 6 18
3 400 1 200
10 400 4 000
400 400 160 000

Las tres primeras filas son irrelevantes para un ordenador. La última ilustra el peligro: cuando ambos bucles recorren la misma lista de datos, el coste crece con el cuadrado del tamaño. Doblar los datos cuadruplica el trabajo. Eso se llama coste cuadrático y es la causa más frecuente de que una interfaz se quede congelada; lo estudiarás con detalle en Optimización del Rendimiento.

Dos reglas prácticas para el nivel en el que estás:

  1. Anidar un bucle pequeño (el equipo: 3) con uno grande (las tareas: 400) es perfectamente aceptable. El coste es 3 × 400, es decir, lineal con un factor constante.
  2. Anidar dos bucles sobre la misma lista grande es la señal de alarma. Comparar cada tarea con todas las demás —para detectar títulos duplicados, por ejemplo— es exactamente ese caso. Suele existir una solución con un solo recorrido y un objeto de consulta; lo verás en el Módulo 4.

Un tercer nivel de anidamiento (personas × estados × tareas) casi siempre indica que el problema se puede reformular. Si te encuentras escribiendo el tercer for, párate a pensar.

  1. break y continue dentro de bucles anidados

Regla fundamental: break y continue afectan únicamente al bucle más interno que los contiene.

for (let p = 0; p < equipo.length; p++) {
  for (let t = 0; t < ids.length; t++) {
    if (responsables[t] === equipo[p] && fechasLimite[t] < HOY && estados[t] !== 'hecha') {
      console.log(`${equipo[p]} tiene una tarea vencida: ${titulos[t]}`);
      break;      // ← sale del bucle de TAREAS, no del de PERSONAS
    }
  }
}

Salida:

Iván tiene una tarea vencida: Presupuesto de la carpintería

El break corta la búsqueda dentro de la persona actual —una tarea vencida por persona basta para el aviso— y el bucle externo continúa con la siguiente. Es exactamente el comportamiento que queremos aquí, y en la gran mayoría de casos reales.

El problema aparece cuando quieres abandonar los dos bucles a la vez: por ejemplo, parar en cuanto encuentres la primera persona con una tarea vencida, sin seguir revisando al resto del equipo. Con break a secas no se puede. La solución tradicional es una bandera comprobada en ambos niveles:

let culpable = '';

for (let p = 0; p < equipo.length && culpable === ''; p++) {
  for (let t = 0; t < ids.length; t++) {
    if (responsables[t] === equipo[p] && fechasLimite[t] < HOY && estados[t] !== 'hecha') {
      culpable = equipo[p];
      break;
    }
  }
}

console.log(culpable !== '' ? `Primer aviso para ${culpable}` : 'Todo en plazo');
// Primer aviso para Iván

La condición extra && culpable === '' en la cabecera del bucle externo hace el trabajo. Funciona, pero hay que acordarse de escribirla, y con tres niveles de anidamiento se vuelve incómoda. Para eso existen las etiquetas.

  1. Etiquetas: break etiqueta y continue etiqueta

Una etiqueta (label) es un nombre seguido de dos puntos colocado justo antes de un bucle. Permite que break y continue indiquen a qué bucle se refieren.

let culpable = '';

busqueda:                                          // ← la etiqueta
for (let p = 0; p < equipo.length; p++) {
  for (let t = 0; t < ids.length; t++) {
    if (responsables[t] === equipo[p] && fechasLimite[t] < HOY && estados[t] !== 'hecha') {
      culpable = equipo[p];
      break busqueda;                              // ← sale de AMBOS bucles
    }
  }
}

console.log(`Primer aviso para ${culpable}`);      // Primer aviso para Iván

break busqueda; termina el bucle etiquetado como busqueda, es decir, el externo, y con él todo lo que contiene. La ejecución continúa en el console.log.

continue etiqueta es menos frecuente pero también existe: salta a la siguiente iteración del bucle etiquetado.

revisionPersona:
for (let p = 0; p < equipo.length; p++) {
  for (let t = 0; t < ids.length; t++) {
    if (responsables[t] !== equipo[p]) continue;

    if (horas[t] > 40) {
      console.error(`${equipo[p]}: tarea que incumple R3. Se salta esta persona.`);
      continue revisionPersona;    // ← abandona esta persona, pasa a la siguiente
    }
    console.log(`${equipo[p]} · ${titulos[t]}`);
  }
}

Detalles de sintaxis y uso:

  • La etiqueta va inmediatamente antes del bucle, normalmente en su propia línea y sin indentar.
  • El nombre sigue las reglas de los identificadores de la lección 01-04, y por convención se escribe en minúsculas o camelCase describiendo el propósito: busqueda, revisionPersona.
  • break etiqueta y continue etiqueta solo pueden usarse dentro del bucle etiquetado.
  • Técnicamente una etiqueta puede marcar cualquier bloque, no solo un bucle, pero eso es rarísimo y no lo necesitarás.

Por qué se usan poco. Las etiquetas son código correcto y legal, pero muchos equipos las prohíben en su guía de estilo, por dos motivos:

  1. Son primas del goto, y saltar a un punto marcado desde el interior de varios bloques rompe la lectura secuencial del código.
  2. Casi siempre hay una alternativa mejor, y esa alternativa suele ser extraer el bucle interno a una función y salir con return —lo verás en Definición y Llamada de Funciones—.

Cuándo son la solución limpia: cuando tienes dos o tres bucles anidados, la salida temprana afecta a todos y extraer una función complicaría el código más de lo que lo simplifica. En esa situación concreta, break etiqueta es más claro que tres banderas comprobadas en tres cabeceras.

  1. Alternativas más legibles que las etiquetas

Antes de recurrir a una etiqueta, considera estas tres opciones en orden:

1. Bandera en la condición del bucle externo. Es la de la sección 8. Ventaja: la condición de parada está visible en la cabecera. Inconveniente: hay que recordar escribirla en los dos sitios.

2. Extraer la lógica a una función y usar return. La opción que usarás a partir del Módulo 3. return sale de la función entera, atravesando todos los bucles de golpe, y además da un nombre a la operación:

// Adelanto del Módulo 3 — no hace falta que lo entiendas aún
function buscarPrimerCulpable(equipo, responsables, fechasLimite, estados, hoy) {
  for (let p = 0; p < equipo.length; p++) {
    for (let t = 0; t < responsables.length; t++) {
      if (responsables[t] === equipo[p] && fechasLimite[t] < hoy && estados[t] !== 'hecha') {
        return equipo[p];      // sale de los dos bucles y de la función
      }
    }
  }
  return null;
}

Compara esto con la versión etiquetada: el nombre buscarPrimerCulpable documenta la intención, el resultado se puede reutilizar y no hay ninguna etiqueta que interpretar. Esta es la razón de fondo por la que las etiquetas se ven tan poco en código moderno.

3. Reformular para no necesitar el anidamiento. Muchas veces el segundo bucle sobra. El ejemplo del culpable puede resolverse con un solo recorrido de las tareas, porque cada tarea ya sabe quién es su responsable:

let culpable = '';

for (let t = 0; t < ids.length; t++) {
  if (fechasLimite[t] < HOY && estados[t] !== 'hecha') {
    culpable = responsables[t];
    break;
  }
}

console.log(`Primer aviso para ${culpable}`);   // Primer aviso para Iván

Seis iteraciones en el peor caso en lugar de dieciocho, un solo break sin etiqueta y menos código. Cuando un bucle anidado te obliga a usar etiquetas, sospecha primero del anidamiento. Muy a menudo el problema se puede recorrer una sola vez.

  1. Caso práctico: la matriz responsable × estado

El tablero de Nómada Tareas necesita una tabla cruzada: cuántas tareas tiene cada persona en cada estado. Es el ejemplo canónico de bucle anidado, esta vez cruzando dos listas de configuración —equipo y ESTADOS— contra la lista de datos.

const equipo = ['Marta', 'Iván', 'Lucía'];
const ESTADOS = ['pendiente', 'en-curso', 'hecha'];

console.log('Persona   pendiente  en-curso  hecha   TOTAL');
console.log('------------------------------------------------');

const totalesPorEstado = [0, 0, 0];

for (let p = 0; p < equipo.length; p++) {
  let fila = equipo[p].padEnd(10);
  let totalPersona = 0;

  for (let e = 0; e < ESTADOS.length; e++) {
    let cuenta = 0;

    for (let t = 0; t < ids.length; t++) {
      if (archivadas[t]) continue;
      if (responsables[t] !== equipo[p]) continue;
      if (estados[t] !== ESTADOS[e]) continue;
      cuenta++;
    }

    fila += String(cuenta).padEnd(11);
    totalPersona += cuenta;
    totalesPorEstado[e] += cuenta;
  }

  console.log(fila + totalPersona);
}

console.log('------------------------------------------------');
console.log(
  'TOTAL     ' +
  String(totalesPorEstado[0]).padEnd(11) +
  String(totalesPorEstado[1]).padEnd(10) +
  String(totalesPorEstado[2]).padEnd(8) +
  (totalesPorEstado[0] + totalesPorEstado[1] + totalesPorEstado[2])
);

Salida:

Persona   pendiente  en-curso  hecha   TOTAL
------------------------------------------------
Marta     1          0          0          1
Iván      1          2          0          3
Lucía     1          0          0          1
------------------------------------------------
TOTAL     3          2         0       5

Qué está pasando aquí, capa por capa:

  • Tres niveles de anidamiento: 3 personas × 3 estados × 6 tareas = 54 iteraciones internas. Con tres personas y tres estados fijos, esos factores son constantes, así que el coste real crece solo con el número de tareas.
  • Las tres guardas con continue descartan lo que no encaja en la celda actual. Escritas como una sola condición serían !archivadas[t] && responsables[t] === equipo[p] && estados[t] === ESTADOS[e], correcta pero mucho menos legible.
  • La tarea 4 (Inventario de tintas) no aparece en ninguna celda porque está archivada: la primera guarda la excluye. Por eso la columna hecha está a cero y el total es 5 y no 6.
  • totalesPorEstado es un array acumulador: se declara antes de todo y se incrementa por índice de estado, sumando las columnas a medida que se calculan.
  • fila se construye por concatenación dentro del bucle de estados y se imprime una sola vez por persona. Acumular texto en una variable es el mismo patrón acumulador de la lección 02-02, con '' como valor neutro.

Este tercer nivel de anidamiento es justo lo que la sección 7 aconsejaba evitar, y tiene solución: recorrer las tareas una sola vez e ir sumando en la celda correspondiente. Pero eso exige indexar por dos claves a la vez, que es precisamente lo que hacen los objetos del Módulo 4. Por ahora, tres bucles sobre seis tareas es una solución correcta y perfectamente asumible.

Errores Comunes y Consejos

Creer que break sale de todos los bucles. Sale solo del más interno. Para salir de varios, etiqueta o bandera.

Usar break dentro de un switch esperando salir del bucle. Ese break pertenece al switch. Es un fallo silencioso y muy difícil de ver.

continue en un while sin haber actualizado el contador. Bucle infinito garantizado. Si necesitas continue, usa for.

Reutilizar la variable de control en bucles anidados.

// ✗ El bucle interno destruye el índice del externo
for (let i = 0; i < equipo.length; i++) {
  for (let i = 0; i < ids.length; i++) { }   // sombrea al externo; comportamiento confuso
}

Usa nombres distintos y, mejor aún, descriptivos: p para personas, t para tareas, e para estados.

Declarar el acumulador en el nivel equivocado. En un bucle anidado, la posición de la declaración decide el significado: fuera de todo es el total global, dentro del externo es el total por persona, dentro del interno no acumula nada. Es la fuente número uno de resultados extraños en tablas cruzadas.

Poner código después de un break en el mismo bloque. Nunca se ejecuta. Algunos editores lo marcan en gris; si ves código gris, hay lógica muerta.

Abusar de continue. Tres guardas al principio del cuerpo son excelentes; ocho continue repartidos por cincuenta líneas convierten el bucle en un laberinto. Si llegas a ese punto, el bucle necesita partirse en funciones.

Consejo: pon el break lo más cerca posible de la condición que lo justifica. Un break al final de un cuerpo largo es difícil de relacionar con su causa.

Consejo: comenta por qué sales, no que sales. break; // ya la hemos encontrado no aporta nada; break; // basta un aviso por persona sí.

Consejo: para depurar un bucle anidado, imprime los dos índices juntos. console.log(p, t, equipo[p], titulos[t]) como primera línea del cuerpo interno te muestra el recorrido completo y revela al instante si un contador está en el nivel equivocado.

Ejercicios

Ejercicio 1 — Primera tarea disponible para alguien

Escribe un bucle que encuentre la primera tarea pendiente, no archivada y sin vencer que podría empezar Lucía, cuya carga actual es la más baja del equipo. Usa continue para los descartes y break para parar en cuanto la encuentres. Muestra su id, su título y las horas. Si no hay ninguna, indícalo.

Ejercicio 2 — Reparto equilibrado

Recorre el equipo con un bucle anidado y calcula, para cada persona, sus horas abiertas (estado distinto de 'hecha' y no archivada). Después determina quién tiene más horas y quién tiene menos, y sugiere mover una tarea. Ten cuidado con dónde declaras cada acumulador.

Ejercicio 3 — Etiquetas contra reformulación

El taller no admite dos tareas abiertas con la misma prioridad y el mismo responsable venciendo el mismo mes. Escribe un bucle anidado que compare cada tarea con las posteriores y se detenga en el primer conflicto, usando break etiqueta. Después responde: ¿cuántas comparaciones hace este algoritmo con 6 tareas? ¿Y con 400?

Soluciones

Ejercicio 1

let idDisponible = 0;
let tituloDisponible = '';
let horasDisponible = 0;

for (let t = 0; t < ids.length; t++) {
  if (archivadas[t]) continue;                    // fuera del tablero
  if (estados[t] !== 'pendiente') continue;       // ya empezada o terminada
  if (fechasLimite[t] < HOY) continue;            // vencida: la reasigna Marta

  idDisponible = ids[t];
  tituloDisponible = titulos[t];
  horasDisponible = horas[t];
  break;                                          // basta con la primera
}

if (idDisponible === 0) {
  console.log('No hay ninguna tarea disponible para asignar a Lucía.');
} else {
  console.log(`Sugerencia para Lucía: [${idDisponible}] ${tituloDisponible} (${horasDisponible} h)`);
}
// Sugerencia para Lucía: [2] Cartelería del taller de serigrafía (6 h)

El recorrido descarta la tarea 1 (está en-curso) y se queda con la 2, que es pendiente, no está archivada y vence el 2026-10-15, aún por delante de HOY. Dos iteraciones de seis.

Las tres guardas están ordenadas de más barata y más excluyente a menos: primero un booleano directo, después una comparación de textos, y por último la de fechas. No cambia el resultado, pero es un buen hábito: cuando las comprobaciones sean costosas, el orden importará.

idDisponible = 0 funciona como centinela porque la regla R1 garantiza que los identificadores son enteros positivos: el 0 no puede ser un id real.

Ejercicio 2

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

let maxHoras = -1;
let personaMax = '';
let minHoras = Infinity;
let personaMin = '';

for (let p = 0; p < equipo.length; p++) {
  let horasPersona = 0;                    // ← se reinicia en cada persona

  for (let t = 0; t < ids.length; t++) {
    if (archivadas[t]) continue;
    if (estados[t] === 'hecha') continue;
    if (responsables[t] !== equipo[p]) continue;
    horasPersona += horas[t];
  }

  console.log(`${equipo[p].padEnd(6)} ${horasPersona} h abiertas`);

  if (horasPersona > maxHoras) {
    maxHoras = horasPersona;
    personaMax = equipo[p];
  }
  if (horasPersona < minHoras) {
    minHoras = horasPersona;
    personaMin = equipo[p];
  }
}

console.log(`\nMás cargado:  ${personaMax} (${maxHoras} h)`);
console.log(`Menos cargado: ${personaMin} (${minHoras} h)`);
console.log(`Desequilibrio: ${maxHoras - minHoras} h`);

if (maxHoras - minHoras > 10) {
  console.warn(`Marta debería mover alguna tarea de ${personaMax} a ${personaMin}.`);
}

Salida:

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

Más cargado:  Iván (25 h)
Menos cargado: Marta (6 h)
Desequilibrio: 19 h

Lo importante es dónde vive cada variable:

Variable Dónde se declara Por qué
horasPersona Dentro del bucle externo Debe empezar en 0 para cada persona
maxHoras, personaMax Fuera de ambos bucles Deben sobrevivir a todas las personas

Si horasPersona se declarara fuera, iría acumulando el total del equipo y cada fila mostraría un número mayor que la anterior: 6, 31, 45. Es un error que produce números plausibles y por eso cuesta detectarlo. Si maxHoras se declarara dentro, se reiniciaría en cada persona y el resultado final sería siempre el de la última.

Infinity como valor inicial de minHoras es el centinela idóneo para un mínimo: cualquier número real es menor, así que la primera persona siempre lo sustituye. Lo conociste en Variables y Tipos de Datos.

Ejercicio 3

let conflicto = false;
let tareaA = 0;
let tareaB = 0;

comparacion:
for (let a = 0; a < ids.length; a++) {
  if (estados[a] === 'hecha' || archivadas[a]) continue;

  for (let b = a + 1; b < ids.length; b++) {          // ← b empieza en a + 1
    if (estados[b] === 'hecha' || archivadas[b]) continue;

    const mismaPrioridad = prioridades[a] === prioridades[b];
    const mismoResponsable = responsables[a] === responsables[b];
    const mismoMes = fechasLimite[a].slice(0, 7) === fechasLimite[b].slice(0, 7);

    if (mismaPrioridad && mismoResponsable && mismoMes) {
      conflicto = true;
      tareaA = ids[a];
      tareaB = ids[b];
      break comparacion;                              // ← sale de los dos bucles
    }
  }
}

if (conflicto) {
  console.error(`Conflicto de planificación entre las tareas ${tareaA} y ${tareaB}.`);
} else {
  console.log('No hay conflictos de planificación.');
}
// Conflicto de planificación entre las tareas 1 y 6.

fechasLimite[a].slice(0, 7) se queda con los siete primeros caracteres de la fecha ISO, es decir, '2026-09': año y mes. Es la forma más simple de comparar "mismo mes" cuando las fechas son texto en formato ISO.

El backlog sí tiene un conflicto: la tarea 1 (Rediseñar la sala polivalente, Iván, prioridad alta, vence el 2026-09-30) y la tarea 6 (Presupuesto de la carpintería, Iván, prioridad alta, vence el 2026-09-05) comparten responsable, prioridad y mes. El algoritmo lo detecta al comparar la pareja a = 0, b = 5, y la etiqueta corta ahí los dos bucles.

Este resultado ilustra algo valioso: al aplicar una regla de negocio nueva sobre datos reales suelen aparecer conflictos que nadie había visto. Marta tendría que decidir si mueve una de las dos tareas a octubre o si relaja la regla.

El detalle clave del algoritmo es let b = a + 1. Si b empezara en 0, cada pareja se compararía dos veces (A con B y B con A) y, peor aún, cada tarea se compararía consigo misma, produciendo un conflicto falso siempre. Empezar en a + 1 compara cada pareja exactamente una vez.

Número de comparaciones: con n tareas, el bucle hace n × (n - 1) / 2 comparaciones.

Tareas Comparaciones
6 15
40 780
400 79 800

Multiplicar por 10 el número de tareas multiplica por 100 el trabajo: es el coste cuadrático de la sección 7 en estado puro. Con 400 tareas y un cálculo tan simple sigue siendo instantáneo, pero si dentro del bucle hubiera que pintar algo o pedir datos a un servidor, la aplicación se pararía. La solución —agrupar las tareas por la clave responsable + prioridad + mes en un solo recorrido— llega en el Módulo 4.

Y observa que en este caso concreto la etiqueta sí está justificada: la salida temprana afecta a dos bucles, la bandera exigiría comprobarla en las dos cabeceras y todavía no tienes funciones para extraer la búsqueda. Es exactamente el nicho de break etiqueta.

Conclusión

Ya controlas el flujo dentro de un bucle con precisión. Sabes que break termina el bucle en el acto y que es la herramienta correcta para "la primera coincidencia" y para "¿existe alguna?", no solo por velocidad sino porque seguir iterando después de encontrar la respuesta puede duplicar efectos secundarios. Sabes que continue salta a la siguiente iteración y que su mejor uso son las guardas al principio del cuerpo, que mantienen el código plano y separan los descartes de la lógica principal. Y conoces sus dos trampas: continue en un while sin actualizar el contador provoca un bucle infinito, y un break dentro de un switch sale del switch, no del bucle.

Sabes anidar bucles para cruzar dos listas —el equipo contra el backlog—, colocar cada acumulador en el nivel correcto y estimar el coste: aceptable cuando uno de los bucles es pequeño y fijo, peligroso cuando ambos recorren la misma lista grande. Conoces las etiquetas, break etiqueta y continue etiqueta, sabes cuándo son la solución limpia y por qué en la mayoría de los casos hay algo mejor: una bandera en la cabecera, un solo recorrido bien planteado o, a partir del Módulo 3, una función con return.

Aplicado a Nómada Tareas, ya has construido la matriz responsable × estado del tablero, has detectado el desequilibrio de carga entre Iván y Marta y has escrito la primera regla de planificación que compara tareas entre sí.

Todo esto asume una cosa: que los datos son correctos. Pero ¿qué ocurre cuando el título llega vacío, cuando horasEstimadas es el texto 'doce' o cuando alguien intenta leer un campo de una tarea que no existe? Hasta ahora tu código habría producido un NaN silencioso o se habría detenido con un error incomprensible. En Manejo de Errores con try-catch, la última lección del módulo, aprenderás a detectar esas situaciones, a lanzar errores con mensajes útiles y a decidir qué hacer cuando algo va mal, sin que la aplicación entera se venga abajo.

Curso de JavaScript: De Principiante a Avanzado

Módulo 1: Introducción a JavaScript

Módulo 2: Estructuras de Control

Módulo 3: Funciones

Módulo 4: Objetos y Arrays

Módulo 5: Objetos y Funciones Avanzadas

Módulo 6: El Modelo de Objetos del Documento (DOM)

Módulo 7: APIs del Navegador y Temas Avanzados

Módulo 8: Pruebas y Depuración

Módulo 9: Rendimiento y Optimización

Módulo 10: Frameworks y Librerías de JavaScript

Módulo 11: Proyecto Final

© Copyright 2026. Todos los derechos reservados