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
- Errores de sintaxis y excepciones: no son lo mismo
- Qué ocurre cuando nadie captura una excepción
tryycatchfinallyy el flujo completo- El objeto
Error:name,messageystack - Los tipos de error incorporados
throw: lanzar tus propios errores- Por qué se lanza un
Errory no una cadena - Errores personalizados: la receta breve
- Validar entradas y fallar pronto
- Qué NO capturar: silenciar errores es un antipatrón
- Los errores asíncronos son otra historia
- Caso práctico: aceptar tareas nuevas del formulario
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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 |
Sí |
| 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"]
- 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 ejecutaSalida:
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.
try y catch
try y catchLa estructura básica tiene dos bloques:
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
tryse ejecuta normalmente hasta que algo falla. Si nada falla,catchse salta por completo. - Cuando algo falla, la ejecución salta al
catchinmediatamente. El resto deltryno se ejecuta: en el ejemplo, elconsole.log('Tareas recuperadas...')nunca llega a correr. errores un parámetro que recibe el objeto que describe el fallo. Puedes llamarlo como quieras;error,erryeson 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.
finally y el flujo completo
finally y el flujo completofinally 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.
- El objeto
Error: name, message y stack
Error: name, message y stackLo 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ó.
- 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); } // SyntaxErrorConocer 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.
throw: lanzar tus propios errores
throw: lanzar tus propios erroresHasta 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:
- Detiene la ejecución en ese punto. Como el
breakde la lección anterior, pero mucho más radical: no salta a la siguiente iteración, abandona todo el bloquetry. - Busca el
catchmás cercano que envuelva esa línea. - Si no hay ninguno, el error queda
Uncaughty 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.
- Por qué se lanza un
Error y no una cadena
Error y no una cadenathrow acepta cualquier valor. Esto es legal:
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.
- 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.
- 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 hEl 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.
- 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.
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.
- 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.
- 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
tryestá dentro del bucle, no fuera. Esta es la clave de todo el ejemplo. Con eltryenvolviendo 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. throwfunciona 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 variableerrorque arrastrar, no hayelse.- Cada
throwelige su tipo de error.TypeErrorpara un tipo equivocado,RangeErrorpara valores fuera de rango,Errorgenérico para el resto de reglas de negocio. Con eso, elcatchpodrí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
forexterno que recorre las solicitudes, elforinterno conbreakque busca al responsable en el equipo, losifde cada validación y eltry/catchque 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]':
Con '{ roto':
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:
Con en-curso → 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:
error.campopermite 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.JSON.stringify(error.valorRecibido)muestra el valor entre comillas, lo que distingue el texto vacío""de un espacio" "o denull. Unconsole.lognormal imprimiría los tres de forma indistinguible.- La rama
elseno silencia nada. Los errores que no son de validación —unTypeErrorpor 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
- ¿Qué es JavaScript?
- Configuración de tu Entorno de Desarrollo
- Tu Primer Programa en JavaScript
- Sintaxis y Conceptos Básicos de JavaScript
- Variables y Tipos de Datos
- Operadores Básicos
- Conversión de Tipos y Comparaciones
- El Proyecto del Curso: Nómada Tareas
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for, while, do-while
- Sentencias Switch
- Control del Flujo: break, continue y Bucles Anidados
- Manejo de Errores con try-catch
Módulo 3: Funciones
- Definición y Llamada de Funciones
- Expresiones de Función y Funciones Flecha
- Parámetros y Valores de Retorno
- Ámbito y Closures
- Hoisting y el Contexto de Ejecución
- Funciones de Orden Superior
- Recursividad
Módulo 4: Objetos y Arrays
- Introducción a los Objetos
- Métodos de Objeto y la Palabra Clave
this - Arrays: Conceptos Básicos y Métodos
- Iteración sobre Arrays
- Buscar, Ordenar y Agregar Datos: find, sort y reduce
- Desestructuración de Arrays
- Desestructuración de Objetos, Spread y Rest
- JSON y Copias de Objetos
Módulo 5: Objetos y Funciones Avanzadas
- Prototipos y Herencia
- Clases y Programación Orientada a Objetos
- Encapsulación: Getters, Setters y Campos Privados
- Módulos e Importación/Exportación
- JavaScript Asíncrono: Callbacks
- Promesas y Async/Await
- El Bucle de Eventos y la Cola de Microtareas
- Iteradores y Generadores
Módulo 6: El Modelo de Objetos del Documento (DOM)
- Introducción al DOM
- Selección y Manipulación de Elementos del DOM
- Manejo de Eventos
- Propagación, Delegación y Eventos Personalizados
- Creación y Eliminación de Elementos del DOM
- Renderizado de Listas y Plantillas HTML
- Manejo y Validación de Formularios
Módulo 7: APIs del Navegador y Temas Avanzados
- Almacenamiento Local y de Sesión
- Fetch API y AJAX
- Peticiones Robustas: Errores, Timeouts y AbortController
- WebSockets
- Service Workers y Aplicaciones Web Progresivas (PWAs)
- APIs del Navegador Esenciales
- Introducción a WebAssembly
Módulo 8: Pruebas y Depuración
- Depuración de JavaScript
- Calidad de Código: ESLint, Prettier y Convenciones
- Pruebas Unitarias con Jest
- Dobles de Prueba: Mocks, Stubs y Spies
- Pruebas de Integración
- Pruebas de Extremo a Extremo con Cypress
Módulo 9: Rendimiento y Optimización
- Medir Antes de Optimizar: DevTools y Web Vitals
- Optimización del Rendimiento de JavaScript
- Gestión de Memoria
- Manipulación Eficiente del DOM
- Carga Perezosa y División de Código
Módulo 10: Frameworks y Librerías de JavaScript
- Por Qué Existen los Frameworks
- Introducción a React
- Gestión de Estado con Redux
- Conceptos Básicos de Vue.js
- Conceptos Básicos de Angular
- Elegir el Framework Adecuado
