Todo el código que has escrito en este módulo asume que los datos son correctos: que horasEstimadas es un número, que prioridad vale 'alta', 'media' o 'baja', que la fecha límite tiene formato ISO. En cuanto Nómada Tareas tenga un formulario, esa suposición se romperá el primer día: Iván escribirá "dos" donde iban las horas, alguien dejará el título en blanco y otra persona pondrá una fecha del año pasado. Sin protección, el programa hace una de dos cosas, ambas malas: produce un NaN silencioso que contamina todos los cálculos posteriores, o se detiene en seco con un mensaje que Marta no entiende. En esta lección aprenderás a detectar esas situaciones, a interrumpir la ejecución con un error propio y un mensaje útil, y a capturarlo para decidir qué hacer sin que la aplicación entera se venga abajo.

Contenido

  1. Errores de sintaxis y excepciones: no son lo mismo
  2. Qué ocurre cuando nadie captura una excepción
  3. try y catch
  4. finally y el flujo completo
  5. El objeto Error: name, message y stack
  6. Los tipos de error incorporados
  7. throw: lanzar tus propios errores
  8. Por qué se lanza un Error y no una cadena
  9. Errores personalizados: la receta breve
  10. Validar entradas y fallar pronto
  11. Qué NO capturar: silenciar errores es un antipatrón
  12. Los errores asíncronos son otra historia
  13. Caso práctico: aceptar tareas nuevas del formulario
  14. Errores Comunes y Consejos
  15. Ejercicios
  16. Conclusión

  1. Errores de sintaxis y excepciones: no son lo mismo

Cuando en la lección 01-03 aprendiste a leer un error de la consola, no distinguimos entre dos cosas muy distintas. Ahora sí hace falta.

Error de sintaxis Excepción (error en ejecución)
Cuándo aparece Al leer el código, antes de ejecutar nada Durante la ejecución, en una línea concreta
Causa El código no es JavaScript válido Los datos o el estado no son los esperados
Ejemplo const x = ; null.titulo
Cuánto código se ejecuta Ninguno del fichero afectado Todo hasta la línea que falla
¿Se puede capturar? No, con try/catch normal
Cómo se arregla Corrigiendo el código Validando datos o capturando

Un error de sintaxis es un fallo del programador, se detecta antes de ejecutar y no tiene sentido "manejarlo": se corrige. Una excepción es una situación anómala que ocurre mientras el programa funciona, casi siempre porque los datos no eran los que esperabas. Esa es la que se maneja.

flowchart TD
    A["El motor lee el fichero"] --> B{"¿Sintaxis válida?"}
    B -->|no| C["SyntaxError<br/>No se ejecuta NADA"]
    B -->|sí| D["Empieza la ejecución"]
    D --> E{"¿Situación anómala<br/>en alguna línea?"}
    E -->|no| F["El programa termina bien"]
    E -->|sí| G["Se lanza una excepción"]
    G --> H{"¿Hay un try/catch<br/>alrededor?"}
    H -->|sí| I["catch decide qué hacer<br/>el programa continúa"]
    H -->|no| J["El programa se detiene"]

  1. Qué ocurre cuando nadie captura una excepción

Cuando se lanza una excepción y nadie la recoge, la ejecución se detiene ahí mismo. Nada de lo que venía después se ejecuta.

const responsable = null;

console.log('Antes');
console.log(responsable.length);   // ✗ TypeError
console.log('Después');            // ← nunca se ejecuta

Salida:

Antes
Uncaught TypeError: Cannot read properties of null (reading 'length')

La palabra clave es Uncaught: significa "nadie ha capturado esto". En Node.js el proceso termina con código de error; en el navegador se detiene el bloque de código actual, aunque el resto de la página sigue viva y los botones que ya tenían escuchadores seguirán respondiendo.

Ese comportamiento —detenerse— es deliberado y correcto. Si el programa siguiera adelante con datos rotos, produciría resultados falsos que nadie detectaría. Es preferible parar en el punto exacto del fallo.

Lo que aporta try/catch no es evitar la parada: es decidir qué se para. Que una tarea del formulario esté mal no debería impedir procesar las otras cuatro.

  1. try y catch

La estructura básica tiene dos bloques:

try {
  // Código que puede fallar
} catch (error) {
  // Qué hacer si ha fallado
}

Con un ejemplo del proyecto: los datos de una tarea llegan como texto JSON desde el almacenamiento del navegador y pueden estar corruptos.

const textoGuardado = '{ esto no es JSON válido }';

try {
  const datos = JSON.parse(textoGuardado);
  console.log('Tareas recuperadas:', datos);
} catch (error) {
  console.error('No se pudieron leer las tareas guardadas.');
  console.error('Motivo técnico:', error.message);
}

console.log('La aplicación continúa.');

Salida:

No se pudieron leer las tareas guardadas.
Motivo técnico: Expected property name or '}' in JSON at position 2
La aplicación continúa.

Lo esencial:

  • El bloque try se ejecuta normalmente hasta que algo falla. Si nada falla, catch se salta por completo.
  • Cuando algo falla, la ejecución salta al catch inmediatamente. El resto del try no se ejecuta: en el ejemplo, el console.log('Tareas recuperadas...') nunca llega a correr.
  • error es un parámetro que recibe el objeto que describe el fallo. Puedes llamarlo como quieras; error, err y e son los nombres habituales.
  • El programa continúa después del catch. Esa es toda la magia.

Si no necesitas el objeto de error, JavaScript moderno permite omitir el paréntesis:

try {
  JSON.parse(textoGuardado);
} catch {
  console.warn('Datos corruptos: se empieza con un tablero vacío.');
}

Mantén el try lo más pequeño posible. Envolver cincuenta líneas en un solo try significa que cualquiera de ellas puede haber fallado y el catch no sabe cuál. Envuelve solo la operación que de verdad puede fallar.

  1. finally y el flujo completo

finally es un tercer bloque opcional que se ejecuta siempre: haya fallado algo o no, se haya capturado o no.

let tareasProcesadas = 0;

try {
  console.log('1. Abriendo el tablero...');
  throw new Error('El almacenamiento no responde');
  console.log('2. Esta línea nunca se ejecuta');
} catch (error) {
  console.error(`3. Capturado: ${error.message}`);
} finally {
  console.log('4. Cerrando el tablero (pase lo que pase)');
}

console.log('5. El programa sigue');

Salida:

1. Abriendo el tablero...
3. Capturado: El almacenamiento no responde
4. Cerrando el tablero (pase lo que pase)
5. El programa sigue
flowchart TD
    A["Entra en try"] --> B{"¿Falla algo?"}
    B -->|no| C["Termina el try completo"]
    B -->|sí| D["Salta al catch<br/>El resto del try se descarta"]
    C --> E["finally"]
    D --> E
    E --> F["Continúa después del bloque"]

finally sirve para las tareas de limpieza que deben ocurrir sí o sí: cerrar una conexión, ocultar un indicador de carga, desbloquear un botón. En Nómada Tareas su caso típico será este, ya en el Módulo 6:

// Esquema de lo que harás con el DOM
try {
  // guardar la tarea
} catch (error) {
  // mostrar el mensaje de error al usuario
} finally {
  // volver a habilitar el botón "Guardar", falle o no
}

Sin finally tendrías que repetir esa línea en el try y en el catch. Con él se escribe una vez y se ejecuta seguro.

Detalle importante: finally se ejecuta incluso si el catch vuelve a lanzar el error. Y también se ejecuta si no hay catch en absoluto: try/finally sin catch es una combinación legal y útil cuando quieres limpiar pero dejar que el error suba.

  1. El objeto Error: name, message y stack

Lo que recibe el catch es un objeto con tres propiedades fundamentales:

Propiedad Qué contiene Ejemplo
name El tipo de error 'TypeError'
message La descripción legible 'Cannot read properties of null'
stack La pila de llamadas: dónde ocurrió y cómo se llegó Texto multilínea con ficheros y números de línea
try {
  const tarea = null;
  console.log(tarea.titulo);
} catch (error) {
  console.log('Tipo:     ', error.name);       // TypeError
  console.log('Mensaje:  ', error.message);    // Cannot read properties of null (reading 'titulo')
  console.log('Traza:\n', error.stack);
}

name y message son los que usarás a diario: name para decidir qué hacer y message para explicar qué ha pasado.

stack es la herramienta de diagnóstico. Contiene el recorrido que siguió la ejecución hasta el punto del fallo, con nombres de fichero y números de línea. Se lee de arriba abajo: la primera línea es donde se produjo el error y las siguientes, quién lo provocó. En el Módulo 3, cuando tengas funciones llamando a funciones, la pila se volverá realmente informativa; hoy tiene una o dos líneas.

Registra el error completo, no solo el mensaje. console.error(error) en la consola del navegador muestra el objeto entero con su traza plegable; console.error(error.message) tira esa información a la basura y te deja sin saber dónde ocurrió.

  1. Los tipos de error incorporados

JavaScript define varios tipos de error. Todos comparten name, message y stack, y se diferencian en la clase de problema que representan.

Tipo Cuándo se lanza Ejemplo típico en el proyecto
TypeError Un valor no es del tipo esperado, o se accede a una propiedad de null/undefined tarea.titulo cuando tarea es null
ReferenceError Se usa una variable que no existe o aún no está inicializada Escribir prioridd en lugar de prioridad
RangeError Un valor numérico está fuera del rango admitido new Array(-1); también lo usarás tú para las horas fuera de 0–40
SyntaxError El código no es JavaScript válido Se detecta antes de ejecutar; también lo lanza JSON.parse con texto corrupto
URIError Uso incorrecto de las funciones de codificación de URL decodeURIComponent('%')
EvalError Prácticamente en desuso

Los tres primeros son los que verás el 95 % del tiempo. Compruébalos:

// TypeError
try { null.titulo; } catch (e) { console.log(e.name); }              // TypeError

// ReferenceError
try { console.log(prioridd); } catch (e) { console.log(e.name); }    // ReferenceError

// RangeError
try { new Array(-1); } catch (e) { console.log(e.name); }            // RangeError

// SyntaxError en tiempo de ejecución, vía JSON.parse
try { JSON.parse('{'); } catch (e) { console.log(e.name); }          // SyntaxError

Conocer el tipo permite reaccionar de forma distinta a cada uno:

try {
  JSON.parse(textoGuardado);
} catch (error) {
  if (error.name === 'SyntaxError') {
    console.warn('Los datos guardados están corruptos. Empezamos de cero.');
  } else {
    console.error('Fallo inesperado al leer el tablero:', error);
  }
}

Existe una forma más idiomática de hacer esa comprobación —error instanceof SyntaxError—, pero instanceof pertenece al mundo de los prototipos y las clases, que se estudia en el Módulo 5. Comparar error.name con un texto funciona perfectamente y es lo que usaremos por ahora.

  1. throw: lanzar tus propios errores

Hasta aquí has capturado errores que lanzaba JavaScript. La otra mitad del mecanismo es lanzarlos tú, con throw, cuando detectas que una regla de negocio no se cumple.

const horasEstimadas = 52;

try {
  if (horasEstimadas > 40) {
    throw new RangeError(`R3 incumplida: ${horasEstimadas} h superan el máximo de 40.`);
  }
  console.log('Tarea aceptada.');
} catch (error) {
  console.error(`[${error.name}] ${error.message}`);
}
// [RangeError] R3 incumplida: 52 h superan el máximo de 40.

Qué hace throw, exactamente:

  1. Detiene la ejecución en ese punto. Como el break de la lección anterior, pero mucho más radical: no salta a la siguiente iteración, abandona todo el bloque try.
  2. Busca el catch más cercano que envuelva esa línea.
  3. Si no hay ninguno, el error queda Uncaught y el programa se detiene.

new Error(...) crea el objeto de error. El texto que le pasas se convierte en su message. Puedes usar Error genérico o el tipo que mejor describa el problema: RangeError para valores fuera de rango, TypeError para tipos incorrectos.

Cómo escribir un buen mensaje de error. El mensaje lo leerá alguien que intenta arreglar algo, así que debe contener tres cosas:

Ingrediente Mal Bien
Qué ha fallado 'Error' 'horasEstimadas fuera de rango'
Qué valor lo causó 'Valor inválido' 'se recibió 52'
Qué se esperaba 'debe estar entre 0 y 40'

Todo junto:

throw new RangeError(`horasEstimadas fuera de rango: se recibió ${horasEstimadas}, debe estar entre 0 y 40.`);

Ese mensaje se explica solo, sin abrir el código.

  1. Por qué se lanza un Error y no una cadena

throw acepta cualquier valor. Esto es legal:

// ✗ No lo hagas
throw 'Las horas no son válidas';

Y es una mala idea por tres motivos concretos:

1. Pierdes la traza. Una cadena no tiene stack, así que en el catch no hay forma de saber en qué línea se originó el problema. Con proyectos de más de un fichero, esto convierte la depuración en adivinanza.

2. Pierdes el tipo. error.name es undefined, así que no puedes reaccionar de forma distinta según la clase de problema.

3. Rompes el código que captura. Un catch bien escrito hace error.message. Si le llega una cadena, error.message vale undefined y el mensaje se pierde. Peor: si alguien lanza null, error.message provoca a su vez un TypeError dentro del catch.

Compáralo:

try {
  throw 'algo va mal';
} catch (error) {
  console.log(error.name);      // undefined
  console.log(error.message);   // undefined
  console.log(error);           // algo va mal
}

try {
  throw new Error('algo va mal');
} catch (error) {
  console.log(error.name);      // Error
  console.log(error.message);   // algo va mal
  console.log(error.stack);     // Error: algo va mal\n    at ...
}

Norma sin excepciones: lanza siempre un objeto Error o uno de sus tipos derivados.

  1. Errores personalizados: la receta breve

Cuando quieras distinguir tus errores de negocio de los del lenguaje, puedes crear un tipo propio. La receta es esta, y de momento basta con copiarla:

class ErrorDeValidacion extends Error {
  constructor(mensaje, campo) {
    super(mensaje);              // pasa el mensaje al Error base
    this.name = 'ErrorDeValidacion';
    this.campo = campo;          // dato extra: qué campo falló
  }
}

Se usa igual que cualquier otro error, con la ventaja de llevar información adicional:

const titulo = '   ';

try {
  if (titulo.trim() === '') {
    throw new ErrorDeValidacion('El título no puede estar vacío (R2).', 'titulo');
  }
} catch (error) {
  if (error.name === 'ErrorDeValidacion') {
    console.error(`Campo "${error.campo}": ${error.message}`);
  } else {
    console.error('Error inesperado:', error);
  }
}
// Campo "titulo": El título no puede estar vacío (R2).

La ventaja práctica es doble: puedes filtrar en el catch los errores que sabes tratar y dejar pasar los demás, y puedes adjuntar datos —el campo, el valor recibido, el id de la tarea— que el formulario usará para marcar en rojo el recuadro correcto.

Qué significan exactamente class, extends, constructor, super y this se explica en Clases y POO. Aquí es solo una plantilla que puedes reutilizar cambiando el nombre.

  1. Validar entradas y fallar pronto

El principio se llama fail fast: comprobar los datos en el momento en que entran al sistema y rechazarlos ahí mismo, en lugar de dejarlos avanzar y descubrir el problema tres capas más adentro.

La diferencia se ve mejor con un ejemplo. Sin validación:

const horasTexto = 'doce';                  // llega del formulario
const horas = Number(horasTexto);           // NaN

const totalBacklog = 45 + horas;            // NaN
const media = totalBacklog / 6;             // NaN
console.log(`Media por tarea: ${media} h`); // Media por tarea: NaN h

El NaN no provoca ningún error: se propaga en silencio por todos los cálculos y aparece al final, en un informe, sin ninguna pista de cuál fue la tarea culpable. Este es exactamente el escenario contra el que te previno la lección 01-07.

Con validación en la entrada:

const horasTexto = 'doce';

try {
  const horas = Number(horasTexto);

  if (Number.isNaN(horas)) {
    throw new TypeError(`horasEstimadas debe ser un número; se recibió "${horasTexto}".`);
  }
  if (horas <= 0 || horas > 40) {
    throw new RangeError(`horasEstimadas fuera de rango: ${horas}. Debe estar entre 0 y 40.`);
  }

  console.log(`Horas aceptadas: ${horas}`);
} catch (error) {
  console.error(`Tarea rechazada — [${error.name}] ${error.message}`);
}
// Tarea rechazada — [TypeError] horasEstimadas debe ser un número; se recibió "doce".

El fallo se detecta en el punto exacto donde entró el dato malo, el mensaje nombra el campo y el valor recibido, y ningún cálculo posterior llega a contaminarse.

Fíjate en que se usa Number.isNaN(horas) y no isNaN(horas): la distinción de la lección 01-07 sigue siendo la correcta, porque isNaN convierte antes de comprobar y da falsos positivos.

El orden de las validaciones importa. Primero el tipo, después el rango. Comprobar horas > 40 cuando horas es NaN da false, y la validación pasaría sin detectar nada: todas las comparaciones con NaN son falsas.

  1. Qué NO capturar: silenciar errores es un antipatrón

try/catch es una herramienta poderosa, y como toda herramienta poderosa se usa mal con facilidad. Estos son los tres abusos que hay que evitar.

1. El catch vacío. El peor de todos.

// ✗ NUNCA
try {
  procesarTarea();
} catch (error) { }

Esto no maneja el error: lo oculta. El programa continúa con un estado incorrecto, sin dejar ni rastro en la consola, y el fallo emergerá más tarde en otro sitio completamente distinto. Depurar eso puede costar horas. Si de verdad un error concreto es esperable e ignorable, escríbelo explícitamente:

} catch (error) {
  // Es normal que no haya datos guardados la primera vez: empezamos vacíos.
  console.info('Sin tablero previo; se crea uno nuevo.');
}

2. Capturar errores de programación. Un TypeError por escribir mal el nombre de una variable es un bug, no una situación excepcional. Envolverlo en un try/catch no lo arregla: lo esconde. Los bugs se corrigen; solo se capturan las situaciones que el programa no controla.

Situación ¿try/catch? Por qué
Datos que escribe un usuario ✓ Sí No los controlas
Un fichero o una API que puede fallar ✓ Sí Depende del exterior
JSON.parse de datos guardados ✓ Sí Pueden estar corruptos
Un nombre de variable mal escrito ✗ No Es un bug: corrígelo
Un array que sabes que existe ✗ No Si falla, tu lógica está mal

3. El try gigante. Envolver un bloque de cien líneas hace que el catch no pueda saber qué falló ni reaccionar de forma sensata. Envuelve la operación arriesgada, no el programa entero.

Y un patrón que sí es correcto: capturar, enriquecer y volver a lanzar.

try {
  JSON.parse(textoGuardado);
} catch (error) {
  console.error('Fallo al leer el tablero guardado:', error.message);
  throw error;      // no lo silencio: añado contexto y dejo que suba
}

Es lo que harás en capas intermedias: registrar información útil sin decidir tú qué hacer con el problema.

  1. Los errores asíncronos son otra historia

Una advertencia importante para que no te pille por sorpresa más adelante: try/catch solo captura errores del código que se ejecuta ahora mismo, dentro del bloque try. Si dentro del try inicias una operación que terminará más tarde —una petición a un servidor, un temporizador—, el try habrá terminado hace rato cuando esa operación falle, y el catch no se enterará.

// ✗ El catch NO captura nada de esto
try {
  setTimeout(function () {
    throw new Error('Fallo tardío');
  }, 1000);
} catch (error) {
  console.error('Esto no se ejecuta jamás');
}

El error se lanza un segundo después, cuando el try/catch ya no existe, y queda como Uncaught.

El manejo de errores en código asíncrono tiene sus propias herramientas: .catch() en las promesas y try/catch combinado con await, que sí funciona. Todo eso se estudia en Promesas y Async/Await. Por ahora quédate con la regla: try/catch protege lo síncrono.

  1. Caso práctico: aceptar tareas nuevas del formulario

Cerramos el módulo juntando todo lo aprendido. Marta ha recogido cinco solicitudes de tarea en un formulario y hay que procesarlas: aceptar las válidas, rechazar las que incumplan las reglas y —esto es lo importante— que una solicitud mala no impida procesar las siguientes.

Los datos llegan como texto, tal y como salen de un formulario HTML.

const HOY = '2026-09-20';
const EQUIPO = ['Marta', 'Iván', 'Lucía'];

const solTitulos = [
  'Cambiar la lámpara de la sala polivalente',
  '   ',
  'Comprar tinta blanca de serigrafía',
  'Renovar la hoja del torno',
  'Archivar las facturas del trimestre'
];
const solResponsables = ['Lucía', 'Marta', 'Iván', 'Iván', 'Marta'];
const solPrioridades  = ['media', 'alta', 'media', 'urgente', 'baja'];
const solHoras        = ['4', '3', 'dos', '10', '3'];
const solFechas       = ['2026-10-20', '2026-10-01', '2026-10-01', '2026-10-05', '2026-08-01'];

let aceptadas = 0;
let rechazadas = 0;
let horasAceptadas = 0;

for (let i = 0; i < solTitulos.length; i++) {
  try {
    // R2 — título no vacío y de longitud razonable
    const titulo = solTitulos[i].trim();
    if (titulo === '') {
      throw new Error('R2: el título no puede estar vacío.');
    }
    if (titulo.length > 100) {
      throw new RangeError(`R2: el título tiene ${titulo.length} caracteres; el máximo es 100.`);
    }

    // R8 — responsable nulo o del equipo
    const responsable = solResponsables[i];
    let esDelEquipo = false;
    for (let p = 0; p < EQUIPO.length; p++) {
      if (EQUIPO[p] === responsable) {
        esDelEquipo = true;
        break;
      }
    }
    if (responsable !== null && !esDelEquipo) {
      throw new Error(`R8: "${responsable}" no pertenece al equipo de Taller Nómada.`);
    }

    // Prioridad dentro del conjunto cerrado
    const prioridad = solPrioridades[i];
    if (prioridad !== 'alta' && prioridad !== 'media' && prioridad !== 'baja') {
      throw new Error(`Prioridad no válida: "${prioridad}". Use alta, media o baja.`);
    }

    // R3 — horas: primero el tipo, después el rango
    const horas = Number(solHoras[i]);
    if (Number.isNaN(horas)) {
      throw new TypeError(`R3: horasEstimadas debe ser un número; se recibió "${solHoras[i]}".`);
    }
    if (horas <= 0 || horas > 40) {
      throw new RangeError(`R3: ${horas} h fuera de rango. Debe estar entre 0 y 40.`);
    }

    // R4 — la fecha límite no puede estar en el pasado
    const fecha = solFechas[i];
    if (fecha < HOY) {
      throw new RangeError(`R4: la fecha límite ${fecha} es anterior a hoy (${HOY}).`);
    }

    // Si hemos llegado aquí, la solicitud es válida
    aceptadas++;
    horasAceptadas += horas;
    console.log(`✓ Aceptada: ${titulo} · ${responsable} · ${prioridad} · ${horas} h · ${fecha}`);

  } catch (error) {
    rechazadas++;
    console.error(`✗ Solicitud ${i + 1} rechazada — [${error.name}] ${error.message}`);
  }
}

console.log('---');
console.log(`Aceptadas:  ${aceptadas}`);
console.log(`Rechazadas: ${rechazadas}`);
console.log(`Horas añadidas al backlog: ${horasAceptadas} h`);

Salida:

✓ Aceptada: Cambiar la lámpara de la sala polivalente · Lucía · media · 4 h · 2026-10-20
✗ Solicitud 2 rechazada — [Error] R2: el título no puede estar vacío.
✗ Solicitud 3 rechazada — [TypeError] R3: horasEstimadas debe ser un número; se recibió "dos".
✗ Solicitud 4 rechazada — [Error] Prioridad no válida: "urgente". Use alta, media o baja.
✗ Solicitud 5 rechazada — [RangeError] R4: la fecha límite 2026-08-01 es anterior a hoy (2026-09-20).
---
Aceptadas:  1
Rechazadas: 4
Horas añadidas al backlog: 4 h

Repasa las decisiones de diseño, porque son las que aplicarás en todo el proyecto:

  • El try está dentro del bucle, no fuera. Esta es la clave de todo el ejemplo. Con el try envolviendo el bucle entero, la primera solicitud inválida habría abortado el proceso y las tres siguientes no se habrían revisado nunca. Poniéndolo dentro, cada solicitud se procesa de forma independiente y un dato malo solo afecta a su propia fila.
  • throw funciona como una salida temprana perfecta. Es la cláusula de guarda de la lección 02-01, esta vez con una salida real: en cuanto una regla falla, el resto de validaciones de esa solicitud se descartan. No hay anidamiento, no hay variable error que arrastrar, no hay else.
  • Cada throw elige su tipo de error. TypeError para un tipo equivocado, RangeError para valores fuera de rango, Error genérico para el resto de reglas de negocio. Con eso, el catch podría reaccionar distinto a cada familia.
  • El orden de las validaciones es deliberado. El tipo antes que el rango, siempre. Y las comprobaciones baratas antes que las caras, como aprendiste en la lección 02-04.
  • Aparecen las cuatro estructuras del módulo. El for externo que recorre las solicitudes, el for interno con break que busca al responsable en el equipo, los if de cada validación y el try/catch que lo envuelve todo. Ese es el módulo completo funcionando junto.

Y ahora fíjate en algo incómodo: el bloque de validación tiene cuarenta líneas y solo sirve para esto. Si mañana hay que validar una tarea editada en lugar de una nueva, hay que copiarlo entero.

Errores Comunes y Consejos

El catch vacío. Ya lo has visto, pero merece repetirse: es el peor error de esta lección. Un catch que no registra nada convierte un fallo localizado en un misterio.

Lanzar cadenas en lugar de objetos Error. Pierdes name y stack, y rompes cualquier catch que espere un error de verdad.

Poner el try fuera del bucle cuando debía ir dentro. Una entrada mala aborta el proceso completo. Pregúntate siempre: ¿un fallo aquí debe detenerlo todo o solo esta iteración?

Usar try/catch para lo que resuelve un if. Comprobar si un valor es negativo no requiere excepciones: requiere una condición. Las excepciones son para lo excepcional; si el "error" ocurre en el flujo normal, es un caso, no una excepción.

Esperar que try/catch capture errores asíncronos. No lo hace. setTimeout, fetch y las promesas necesitan otro tratamiento, que verás en el Módulo 5.

Comprobar el rango antes que el tipo. Con NaN todas las comparaciones dan false, así que un valor no numérico pasa cualquier validación de rango sin detectarse.

Registrar solo error.message. Pierdes la traza. Usa console.error(error) cuando estés depurando.

Consejo: escribe el mensaje pensando en quien lo va a leer a las tres de la madrugada. Qué falló, qué valor lo causó y qué se esperaba. Los tres ingredientes, siempre.

Consejo: incluye el identificador de la regla en el mensaje. R3: al principio del texto conecta el error con la especificación del proyecto y ahorra mucho tiempo.

Consejo: prueba tus validaciones con datos malos a propósito. Escribe deliberadamente 'dos', -5, '' y null y comprueba que cada uno produce el mensaje que esperabas. Es el germen de las pruebas unitarias del Módulo 8.

Ejercicios

Ejercicio 1 — Lectura robusta del tablero guardado

Escribe un bloque try/catch/finally que intente leer con JSON.parse el contenido de la variable guardado. Si el texto es válido, muestra un mensaje de éxito; si está corrupto, avisa de que se empieza con un tablero vacío. En finally, imprime siempre "Tablero listo". Pruébalo con '[1,2,3]' y con '{ roto', y explica por qué usar if en lugar de try/catch no sería viable aquí.

Ejercicio 2 — Validador de transiciones que lanza errores

Reescribe la validación de cambio de estado de la lección 02-01 usando throw en lugar de las variables permitido y motivo. Debe lanzar un Error con mensaje descriptivo cuando el estado nuevo no exista, cuando sea igual al actual o cuando la transición no esté permitida por R6. Envuélvelo en un try/catch y pruébalo con pendiente → hecha y con en-curso → hecha.

Ejercicio 3 — Errores personalizados con el campo culpable

Usando la receta de la sección 9, crea ErrorDeValidacion con las propiedades campo y valorRecibido. Valida una tarea con titulo = '', horas = '35' y prioridad = 'baja', y haz que el catch distinga entre tus errores de validación —mostrando el campo culpable— y cualquier otro error inesperado.

Soluciones

Ejercicio 1

const guardado = '{ roto';

try {
  const tablero = JSON.parse(guardado);
  console.log(`Tablero recuperado con ${tablero.length} elemento(s).`);
} catch (error) {
  console.warn(`Datos guardados ilegibles (${error.name}). Se empieza con un tablero vacío.`);
} finally {
  console.log('Tablero listo.');
}

Con '[1,2,3]':

Tablero recuperado con 3 elemento(s).
Tablero listo.

Con '{ roto':

⚠ Datos guardados ilegibles (SyntaxError). Se empieza con un tablero vacío.
Tablero listo.

Por qué no sirve un if: para saber si un texto es JSON válido habría que analizarlo carácter a carácter, es decir, reimplementar JSON.parse entero. No existe ninguna comprobación previa razonable. Este es el caso arquetípico de try/catch: la única forma de saber si la operación funciona es intentarla. Cuando puedas comprobarlo antes con una condición, usa la condición; cuando no, captura.

El finally garantiza que la aplicación continúa por ambos caminos con el mismo mensaje, sin duplicarlo en los dos bloques.

Ejercicio 2

const estadoActual = 'pendiente';
const estadoNuevo = 'hecha';

try {
  const esEstadoValido =
    estadoNuevo === 'pendiente' || estadoNuevo === 'en-curso' || estadoNuevo === 'hecha';

  if (!esEstadoValido) {
    throw new TypeError(`"${estadoNuevo}" no es un estado del sistema.`);
  }
  if (estadoActual === estadoNuevo) {
    throw new Error(`La tarea ya está en estado "${estadoActual}".`);
  }

  const transicionValida =
    (estadoActual === 'pendiente' && estadoNuevo === 'en-curso') ||
    (estadoActual === 'en-curso'  && estadoNuevo === 'hecha') ||
    (estadoActual === 'en-curso'  && estadoNuevo === 'pendiente') ||
    (estadoActual === 'hecha'     && estadoNuevo === 'en-curso');

  if (!transicionValida) {
    throw new Error(`R6: transición no permitida ${estadoActual} → ${estadoNuevo}.`);
  }

  console.log(`✓ Estado actualizado a "${estadoNuevo}".`);
} catch (error) {
  console.error(`✗ [${error.name}] ${error.message}`);
}

Con pendiente → hecha:

✗ [Error] R6: transición no permitida pendiente → hecha.

Con en-curso → hecha:

✓ Estado actualizado a "hecha".

Compara esta versión con la de la lección 02-01: allí hacían falta las variables permitido y motivo, y cada rama tenía que asignar las dos. Aquí cada guarda o lanza o deja pasar, y el camino feliz —el console.log final— está al mismo nivel de indentación que todo lo demás.

Fíjate también en que las cuatro transiciones válidas se han agrupado en una sola expresión booleana con paréntesis, aplicando la norma de la lección 02-01: cuando se mezclan && y ||, los paréntesis van aunque la precedencia ya sea correcta.

Ejercicio 3

class ErrorDeValidacion extends Error {
  constructor(mensaje, campo, valorRecibido) {
    super(mensaje);
    this.name = 'ErrorDeValidacion';
    this.campo = campo;
    this.valorRecibido = valorRecibido;
  }
}

const titulo = '';
const horasTexto = '35';
const prioridad = 'baja';

try {
  if (titulo.trim() === '') {
    throw new ErrorDeValidacion('R2: el título es obligatorio.', 'titulo', titulo);
  }

  const horas = Number(horasTexto);
  if (Number.isNaN(horas)) {
    throw new ErrorDeValidacion('R3: las horas deben ser un número.', 'horasEstimadas', horasTexto);
  }
  if (horas <= 0 || horas > 40) {
    throw new ErrorDeValidacion('R3: las horas deben estar entre 0 y 40.', 'horasEstimadas', horas);
  }

  if (prioridad !== 'alta' && prioridad !== 'media' && prioridad !== 'baja') {
    throw new ErrorDeValidacion('Prioridad no reconocida.', 'prioridad', prioridad);
  }

  console.log('✓ Tarea válida.');
} catch (error) {
  if (error.name === 'ErrorDeValidacion') {
    console.error(`✗ Campo "${error.campo}" — ${error.message} (se recibió: ${JSON.stringify(error.valorRecibido)})`);
  } else {
    console.error('✗ Error inesperado, esto es un bug:', error);
  }
}
// ✗ Campo "titulo" — R2: el título es obligatorio. (se recibió: "")

Tres cosas que hacen valioso este patrón:

  1. error.campo permite que la interfaz reaccione. En el Módulo 6, ese dato marcará en rojo exactamente el recuadro del formulario que hay que corregir, en lugar de mostrar un aviso genérico.
  2. JSON.stringify(error.valorRecibido) muestra el valor entre comillas, lo que distingue el texto vacío "" de un espacio " " o de null. Un console.log normal imprimiría los tres de forma indistinguible.
  3. La rama else no silencia nada. Los errores que no son de validación —un TypeError por un bug tuyo— se registran completos, con su traza. Filtrar lo que sabes tratar y dejar visible lo demás es la diferencia entre manejar errores y esconderlos.

Cambia titulo por 'Renovar el torno' y verás cómo la validación avanza hasta el final e imprime ✓ Tarea válida., porque 35 h está dentro de rango y 'baja' es una prioridad correcta.

Conclusión

Con esta lección cierras el Módulo 2. Ya distingues un error de sintaxis —un fallo del código, que se corrige— de una excepción —una situación anómala en ejecución, que se maneja—. Sabes que una excepción sin capturar detiene el programa, y que try/catch no evita la parada sino que te deja decidir qué se para: en el ejemplo del formulario, una solicitud inválida ya no impide procesar las otras cuatro. Conoces finally para la limpieza que debe ocurrir pase lo que pase, el objeto Error con su name, su message y su stack, y los tipos incorporados que te permiten reaccionar de forma distinta a cada clase de problema.

Sabes lanzar tus propios errores con throw, siempre con un objeto Error y nunca con una cadena, y escribir mensajes que digan qué falló, qué valor lo causó y qué se esperaba. Tienes la receta de los errores personalizados para adjuntar el campo culpable. Y —tan importante como lo anterior— sabes lo que no hay que capturar: un catch vacío no maneja un error, lo esconde; los bugs se corrigen en lugar de envolverse; y los errores asíncronos necesitan las herramientas del Módulo 5.

Mira atrás y comprueba lo que has ganado en estas cinco lecciones. Tu código decide con if/else if y con switch, repite con for, while y do...while, recorre el backlog completo acumulando, contando, buscando extremos y cruzando listas con bucles anidados, corta en el momento justo con break y continue, y se defiende de los datos malos con validaciones que fallan pronto y con mensajes útiles. Eso ya es un programa de verdad.

Y también has ido acumulando una queja, lección tras lección. La cadena que convierte prioridad en un peso la has escrito cuatro veces. La validación de transiciones aparece en dos lecciones casi idéntica. El bloque de cuarenta líneas que valida una tarea nueva no puede reutilizarse para validar una tarea editada sin copiarlo entero. Y cuando quisiste salir de dos bucles a la vez, la solución más limpia resultó ser una que todavía no podías escribir. Todo apunta al mismo sitio: necesitas poder dar un nombre a un trozo de lógica, guardarlo y usarlo tantas veces como quieras, con datos distintos cada vez. Eso es una función, y es exactamente lo que empieza en el Módulo 3: Funciones, con Definición y Llamada de Funciones. A partir de ahí, validarTarea(), calcularUrgencia() y puedeCambiarEstado() dejarán de ser bloques copiados para convertirse en piezas con nombre propio que podrás combinar, probar y reutilizar en todo Nómada Tareas.

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