Módulo 10: Gestión de errores
Objetivos del módulo
- Distinguir errores de sintaxis, de ejecución y lógicos
- Conocer el objeto
Errory sus propiedadesname,message,stackycause - Reconocer los errores nativos:
TypeError,ReferenceError,RangeError,SyntaxError,URIErroryAggregateError - Lanzar errores con
throwy capturarlos contry/catch/finally - Relanzar errores que no sabes tratar y encadenarlos con
{ cause } - Crear tus propias clases de error con información extra
- Usar
Error.isError(ES2026) y entender por qué es más fiable queinstanceof - Tener una primera visión de los errores asíncronos y de los manejadores globales en el navegador y en Node.js
- Aplicar estrategias profesionales: validar entradas, fail fast, separar mensajes para el usuario y logs, y depurar con DevTools
1. Tipos de errores
📘 Concepto: Un programa puede fallar de tres maneras muy distintas, y cada una se detecta en un momento diferente.
| Tipo | Cuándo aparece | Ejemplo | Quién lo detecta |
|---|---|---|---|
| Sintaxis | Antes de ejecutar: el código no se puede ni leer | if (x > 1 { | El motor al cargar el fichero (y tu editor) |
| Ejecución (runtime) | Durante la ejecución, en una línea concreta | null.length | El motor, lanzando una excepción |
| Lógico | Nunca “salta”: el programa funciona, pero da un resultado incorrecto | Usar > en vez de >= | Tú, con pruebas y depuración |
// Error lógico: no hay excepción, pero el resultado es incorrecto
function esMayorDeEdad(edad) {
return edad > 18; // ¡con 18 años debería ser true!
}
console.log(esMayorDeEdad(18)); // false
💡 Consejo: Los errores de sintaxis los detecta el editor (VS Code subraya en rojo) y ESLint (módulo 21). Los lógicos son los más peligrosos porque no avisan: para ellos existen los tests (también en el módulo 21).
Este módulo trata sobre todo los errores de ejecución: las excepciones. Cuando se produce una, JavaScript interrumpe el flujo normal y busca un bloque catch que la capture. Si no lo encuentra, el error llega a la consola y el resto del código de ese bloque no se ejecuta.
console.log('Antes');
const usuario = null;
console.log(usuario.nombre); // TypeError: aquí se detiene la ejecución
console.log('Después'); // nunca se ejecuta
2. El objeto Error
const error = new Error('No se pudo conectar con el servidor');
console.log(error.name); // Error
console.log(error.message); // No se pudo conectar con el servidor
console.log(String(error)); // Error: No se pudo conectar con el servidor
console.log(error.stack.split('\n')[0]); // Error: No se pudo conectar con el servidor
| Propiedad | Contenido | Estándar |
|---|---|---|
name | Tipo de error ('Error', 'TypeError'…) | Sí |
message | Descripción legible del problema | Sí |
cause | El error original que provocó este (ES2022, sección 7) | Sí |
stack | Pila de llamadas: qué funciones se estaban ejecutando y en qué línea | No (todos los motores la ofrecen) |
📘 Concepto: La pila de llamadas (stack trace) es como las migas de pan de Pulgarcito: indica el camino exacto que siguió el programa hasta el error, empezando por la línea donde se produjo. Es la información más útil para depurar.
TypeError: Cannot read properties of undefined (reading 'precio')
at calcularTotal (carrito.js:12:23)
at actualizarResumen (carrito.js:30:17)
at HTMLButtonElement.<anonymous> (app.js:8:5)
Se lee de arriba abajo: el error se produjo en calcularTotal (línea 12, columna 23 de carrito.js), que fue llamada por actualizarResumen, que a su vez fue llamada desde un evento de clic en app.js.
3. Errores nativos
| Tipo | Cuándo se produce | Ejemplo |
|---|---|---|
TypeError | Operación sobre un valor del tipo equivocado | null.x, 5(), asignar a una constante |
ReferenceError | Variable que no existe o usada antes de declararse | variableInexistente |
RangeError | Valor numérico fuera del rango permitido | new Array(-1), recursión infinita |
SyntaxError | Código o datos con sintaxis inválida | JSON.parse('{mal}'), new RegExp('(') |
URIError | Uso incorrecto de funciones de URI | decodeURIComponent('%') |
AggregateError | Agrupa varios errores en uno | Promise.any cuando todas fallan |
EvalError | Histórico, ya no lo lanza el motor | — |
const pruebas = [
() => null.nombre,
() => variableQueNoExiste,
() => (1.5).toFixed(200),
() => JSON.parse('{mal}'),
() => decodeURIComponent('%'),
() => {
const f = 5;
f();
},
];
for (const prueba of pruebas) {
try {
prueba();
} catch (error) {
console.log(`${error.name}: ${error.message}`);
}
}
// TypeError: Cannot read properties of null (reading 'nombre')
// ReferenceError: variableQueNoExiste is not defined
// RangeError: toFixed() digits argument must be between 0 and 100
// SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
// URIError: URI malformed
// TypeError: f is not a function
⚠️ Importante: Los mensajes de error dependen del motor: Firefox y Safari no escriben exactamente lo mismo que Chrome o Node. Por eso tu código nunca debe tomar decisiones comparando
error.message; usa el tipo (nameoinstanceof) o propiedades propias, como verás con los errores personalizados.
3.1. AggregateError
Representa varios errores a la vez. Lo lanza Promise.any (módulo 12) cuando fallan todas las promesas, y puedes crearlo tú para informar de varios problemas juntos:
const problemas = [new TypeError('El nombre debe ser texto'), new RangeError('La edad debe ser positiva')];
const agregado = new AggregateError(problemas, 'El formulario tiene 2 errores');
console.log(agregado.name); // AggregateError
console.log(agregado.message); // El formulario tiene 2 errores
console.log(agregado.errors.map((e) => e.message)); // ['El nombre debe ser texto', 'La edad debe ser positiva']
4. Lanzar errores: throw
throw interrumpe la función y lanza una excepción. Úsalo cuando tu función reciba algo con lo que no puede trabajar:
function dividir(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('dividir() solo acepta números');
}
if (b === 0) {
throw new RangeError('No se puede dividir entre cero');
}
return a / b;
}
console.log(dividir(10, 4)); // 2.5
// dividir(10, 0); // RangeError: No se puede dividir entre cero
⚠️ Importante: JavaScript permite lanzar cualquier valor (
throw 'error',throw 404,throw { codigo: 1 }), pero no lo hagas: pierdesstack,namey la compatibilidad con las herramientas. Lanza siempre instancias deErroro de sus subclases.
try {
throw 'algo ha fallado'; // ❌ un texto, no un Error
} catch (error) {
console.log(typeof error, error.stack); // string undefined (¡sin pila!)
}
5. Capturar errores: try, catch y finally
try {
// Código que puede fallar
} catch (error) {
// Se ejecuta SOLO si algo dentro de try lanzó una excepción
} finally {
// Se ejecuta SIEMPRE: haya error o no, e incluso si hay return
}
function leerConfiguracion(texto) {
console.log('Leyendo configuración...');
try {
const config = JSON.parse(texto);
console.log('Configuración válida');
return config;
} catch (error) {
console.log(`JSON inválido (${error.name}). Uso valores por defecto`);
return { tema: 'claro' };
} finally {
console.log('Lectura terminada');
}
}
console.log(leerConfiguracion('{"tema":"oscuro"}'));
// Leyendo configuración...
// Configuración válida
// Lectura terminada
// { tema: 'oscuro' }
console.log(leerConfiguracion('{tema: oscuro}'));
// Leyendo configuración...
// JSON inválido (SyntaxError). Uso valores por defecto
// Lectura terminada
// { tema: 'claro' }
📘 Concepto:
finallyse usa para liberar recursos o restaurar estados: cerrar una conexión, ocultar un indicador de “cargando”, desbloquear un botón… Se ejecuta aunque eltryo elcatchhaganreturn.
⚠️ Importante: No pongas
returndentro definally: sustituye el valor devuelto portryocatchy, además, se “traga” cualquier error que se estuviera propagando.
5.1. catch sin parámetro
Si no necesitas el objeto de error, puedes omitirlo (ES2019):
function esJsonValido(texto) {
try {
JSON.parse(texto);
return true;
} catch {
return false;
}
}
console.log(esJsonValido('[1, 2]'), esJsonValido('[1, 2')); // true false
5.2. Qué captura (y qué no) un try
- Captura los errores lanzados dentro del bloque, incluidos los de las funciones a las que llama, a cualquier profundidad.
- No captura errores de sintaxis del propio fichero (el código ni siquiera se ejecuta).
- No captura errores que ocurran más tarde, en callbacks asíncronos (sección 9).
💡 Consejo: Pon el
trylo más ajustado posible al código que puede fallar y solo donde sepas qué hacer con el error. Envolver funciones enteras “por si acaso” esconde fallos y dificulta la depuración.
6. Relanzar errores
Un catch captura todo, también errores que no esperabas (un error de programación, por ejemplo). La práctica correcta es tratar los que conoces y relanzar el resto:
function obtenerEdad(texto) {
const datos = JSON.parse(texto);
return datos.usuario.edad;
}
function edadSegura(texto) {
try {
return obtenerEdad(texto);
} catch (error) {
if (error instanceof SyntaxError) {
console.log('Datos corruptos: devuelvo null');
return null;
}
throw error; // no sé tratarlo aquí: que lo gestione quien me llamó
}
}
console.log(edadSegura('{"usuario":{"edad":30}}')); // 30
console.log(edadSegura('esto no es JSON')); // Datos corruptos: devuelvo null → null
try {
edadSegura('{"cliente":{}}'); // falta "usuario": TypeError al leer .edad
} catch (error) {
console.log(`Error inesperado relanzado: ${error.name}`); // Error inesperado relanzado: TypeError
}
⚠️ Importante: El anti-patrón más dañino es el
catchvacío:try { ... } catch {}. El error desaparece sin dejar rastro y el programa sigue en un estado incorrecto. Si de verdad quieres ignorar un error, deja al menos un comentario explicando por qué.
7. Encadenar errores con { cause }
Al relanzar, a menudo quieres dar un mensaje de más alto nivel sin perder el error original. Desde ES2022, el constructor de Error acepta un segundo argumento con la propiedad cause:
function leerPerfil(texto) {
try {
return JSON.parse(texto);
} catch (error) {
throw new Error('No se pudo cargar el perfil del usuario', { cause: error });
}
}
try {
leerPerfil('{"nombre": "Ana",}');
} catch (error) {
console.log(error.message); // No se pudo cargar el perfil del usuario
console.log(error.cause.name); // SyntaxError
console.log(error.cause instanceof SyntaxError); // true
}
Puedes recorrer la cadena completa de causas:
function cadenaDeCausas(error) {
const mensajes = [];
for (let actual = error; actual; actual = actual.cause) {
mensajes.push(`${actual.name}: ${actual.message}`);
}
return mensajes.join('\n ← causado por ');
}
const red = new TypeError('Failed to fetch');
const servicio = new Error('Error al consultar la API de pedidos', { cause: red });
const pantalla = new Error('No se pudo mostrar el historial', { cause: servicio });
console.log(cadenaDeCausas(pantalla));
// Error: No se pudo mostrar el historial
// ← causado por Error: Error al consultar la API de pedidos
// ← causado por TypeError: Failed to fetch
💡 Consejo: Node.js y las DevTools de los navegadores muestran la
causeautomáticamente al imprimir el error conconsole.error. Es la forma estándar de conservar el contexto; no concatenes mensajes a mano.
8. Errores personalizados
Crear tus propias clases de error permite distinguir los fallos de tu aplicación y adjuntar datos útiles:
class ErrorValidacion extends Error {
constructor(campo, mensaje, options) {
super(mensaje, options); // pasa message (y cause) al constructor de Error
this.name = 'ErrorValidacion';
this.campo = campo; // dato extra: qué campo falló
}
}
class ErrorNoEncontrado extends Error {
constructor(recurso, id) {
super(`${recurso} con id ${id} no encontrado`);
this.name = 'ErrorNoEncontrado';
this.recurso = recurso;
this.id = id;
this.status = 404;
}
}
const usuarios = new Map([[1, 'Ana']]);
function buscarUsuario(id) {
if (!Number.isInteger(id)) throw new ErrorValidacion('id', 'El id debe ser un entero');
if (!usuarios.has(id)) throw new ErrorNoEncontrado('Usuario', id);
return usuarios.get(id);
}
for (const id of [1, 'x', 99]) {
try {
console.log(`Encontrado: ${buscarUsuario(id)}`);
} catch (error) {
if (error instanceof ErrorValidacion) {
console.log(`Revisa el campo "${error.campo}": ${error.message}`);
} else if (error instanceof ErrorNoEncontrado) {
console.log(`${error.status} - ${error.message}`);
} else {
throw error;
}
}
}
// Encontrado: Ana
// Revisa el campo "id": El id debe ser un entero
// 404 - Usuario con id 99 no encontrado
8.1. Una jerarquía de errores
En aplicaciones medianas conviene una clase base propia de la que hereden las demás:
class ErrorApp extends Error {
constructor(mensaje, { codigo = 'ERROR_DESCONOCIDO', ...opciones } = {}) {
super(mensaje, opciones);
this.name = new.target.name; // toma automáticamente el nombre de la subclase
this.codigo = codigo;
}
}
class ErrorRed extends ErrorApp {}
class ErrorPermisos extends ErrorApp {}
const error = new ErrorPermisos('No puedes borrar este pedido', { codigo: 'SIN_PERMISO' });
console.log(error.name, error.codigo); // ErrorPermisos SIN_PERMISO
console.log(error instanceof ErrorApp); // true
console.log(error instanceof Error); // true
console.log(String(error)); // ErrorPermisos: No puedes borrar este pedido
💡 Consejo:
this.name = new.target.nameevita repetir el nombre en cada subclase. Si tu código se va a minificar (módulo 20) y los nombres de clase pueden cambiar, escribe elnamecomo texto fijo.
9. Error.isError (ES2026)
¿Cómo saber si un valor es un error? La respuesta clásica es valor instanceof Error, pero tiene dos problemas:
- Falsos positivos: cualquier objeto cuyo prototipo sea
Error.prototypepasa la prueba, aunque no sea un error de verdad. - Falsos negativos entre realms: cada
iframe, cada worker y cada contextonode:vmtiene su propio constructorError. Un error creado en uniframeno esinstanceof Erroren la página principal.
📘 Concepto: Un realm es un “universo” de JavaScript con su propio objeto global y sus propias versiones de
Object,Array,Error… Una página con uniframetiene dos realms. Es el mismo motivo por el que existeArray.isArrayen lugar de confiar eninstanceof Array.
Error.isError(valor) comprueba si el valor es realmente un error, mirando una marca interna que solo tienen los objetos creados por los constructores de error:
import vm from 'node:vm';
// Un error creado en otro realm (en el navegador sería un iframe)
const errorAjeno = vm.runInNewContext('new TypeError("desde otro contexto")');
console.log(errorAjeno instanceof Error); // false ← ¡falso negativo!
console.log(Error.isError(errorAjeno)); // true
// Un objeto que se hace pasar por error
const impostor = Object.create(Error.prototype);
console.log(impostor instanceof Error); // true ← ¡falso positivo!
console.log(Error.isError(impostor)); // false
console.log(Error.isError(new RangeError('x'))); // true
console.log(Error.isError({ name: 'Error', message: 'parezco uno' })); // false
console.log(Error.isError('texto')); // false
Equivalente en el navegador con un iframe:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Error.isError y realms</title>
</head>
<body>
<iframe id="marco" srcdoc="<p>Soy otro realm</p>"></iframe>
<script type="module">
const marco = document.querySelector('#marco');
marco.addEventListener('load', () => {
const ErrorDelIframe = marco.contentWindow.Error;
const error = new ErrorDelIframe('creado en el iframe');
console.log(error instanceof Error); // false
console.log(Error.isError(error)); // true
});
</script>
</body>
</html>
⚠️ Importante:
Error.isErrorestá disponible en Chrome y Edge 134+, Firefox 138+, Safari 18.4+ y Node.js 24.3+. Safari (y Node 24.0-24.2) devuelvenfalsepara losDOMException(errores de las APIs del navegador), una limitación conocida. Si necesitas compatibilidad con versiones anteriores,core-jsofrece un polyfill.
💡 Consejo: Úsalo en código que recibe valores de “cualquier parte”: manejadores globales, bibliotecas, funciones de log. Dentro de tu propia aplicación,
instanceofsigue siendo útil para distinguir tus clases de error.
// Normalizar cualquier cosa lanzada a un Error de verdad
function comoError(valor) {
return Error.isError(valor) ? valor : new Error(`Se lanzó un valor que no es Error: ${String(valor)}`, { cause: valor });
}
try {
throw 404;
} catch (lanzado) {
console.log(comoError(lanzado).message); // Se lanzó un valor que no es Error: 404
}
10. Errores en código asíncrono (adelanto)
try/catch solo captura los errores que ocurren mientras se ejecuta el bloque. Un callback que se ejecuta más tarde queda fuera:
try {
setTimeout(() => {
throw new Error('Fallo dentro del temporizador'); // ¡NO lo captura el catch de fuera!
}, 100);
} catch (error) {
console.log('Esto nunca se muestra');
}
Con promesas y async/await (módulo 12), los errores vuelven a gestionarse de forma natural:
function obtenerDatos(ok) {
return new Promise((resolver, rechazar) => {
setTimeout(() => (ok ? resolver({ id: 1 }) : rechazar(new Error('Servidor no disponible'))), 50);
});
}
// Con .catch()
obtenerDatos(false).catch((error) => console.log(`Con .catch: ${error.message}`));
// Con async/await: try/catch de toda la vida
async function cargar() {
try {
const datos = await obtenerDatos(false);
console.log(datos);
} catch (error) {
console.log(`Con try/await: ${error.message}`);
} finally {
console.log('Ocultar indicador de carga');
}
}
await cargar();
// Con .catch: Servidor no disponible
// Con try/await: Servidor no disponible
// Ocultar indicador de carga
⚠️ Importante: Una promesa rechazada que nadie gestiona produce un rechazo no gestionado (unhandled rejection). En Node.js, por defecto, termina el proceso. Gestiona siempre tus promesas con
awaitdentro detryo con.catch().
11. Manejadores globales: la última red de seguridad
Cuando un error no lo captura nadie, puedes interceptarlo a nivel global para registrarlo (enviarlo a un servicio de monitorización como Sentry) o mostrar un aviso genérico. No sustituyen a un buen try/catch: son la red bajo el trapecio.
11.1. En el navegador
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Manejadores globales de errores</title>
</head>
<body>
<button id="fallar">Provocar error</button>
<button id="rechazar">Provocar rechazo</button>
<script type="module">
// Errores síncronos no capturados
window.addEventListener('error', (evento) => {
console.log('Error global:', evento.message);
console.log('Dónde:', evento.filename, evento.lineno, evento.colno);
console.log('Objeto error:', evento.error);
// evento.preventDefault(); // evitaría que el navegador lo muestre en consola
});
// Promesas rechazadas sin gestionar
window.addEventListener('unhandledrejection', (evento) => {
console.log('Rechazo sin gestionar:', evento.reason);
});
document.querySelector('#fallar').addEventListener('click', () => {
null.propiedad; // TypeError no capturado
});
document.querySelector('#rechazar').addEventListener('click', () => {
Promise.reject(new Error('Nadie me escucha'));
});
</script>
</body>
</html>
📘 Concepto: También existe la forma antigua
window.onerror = (mensaje, origen, linea, columna, error) => { ... }, que recibe los datos como argumentos separados y, si devuelvetrue, evita el mensaje en consola. PrefiereaddEventListener, que permite registrar varios manejadores.
11.2. En Node.js (breve)
process.on('uncaughtException', (error, origen) => {
console.error(`Error no capturado (${origen}): ${error.message}`);
// El estado del programa ya no es fiable: registra y termina
process.exit(1);
});
process.on('unhandledRejection', (motivo) => {
console.error('Promesa rechazada sin gestionar:', motivo);
process.exit(1);
});
⚠️ Importante: Tras un
uncaughtException, la documentación de Node recomienda terminar el proceso (y dejar que un gestor como systemd, Docker o PM2 lo reinicie), no seguir como si nada.
12. Estrategias profesionales
12.1. Validar entradas y fail fast
📘 Concepto: Fail fast (fallar pronto) significa detectar los datos incorrectos en cuanto entran y lanzar un error claro, en lugar de dejar que el valor erróneo viaje por el programa y provoque un fallo misterioso lejos de su origen.
// ❌ Sin validar: el fallo aparece lejos y con un mensaje confuso
function precioFinalMal(producto) {
return producto.precio * (1 - producto.descuento / 100);
}
console.log(precioFinalMal({ precio: '20', descuento: undefined })); // NaN (silencioso)
// ✅ Validar al entrar: error inmediato y explícito
function precioFinal({ precio, descuento = 0 }) {
if (!Number.isFinite(precio) || precio < 0) {
throw new TypeError(`precio debe ser un número positivo (recibido: ${JSON.stringify(precio)})`);
}
if (!Number.isFinite(descuento) || descuento < 0 || descuento > 100) {
throw new RangeError(`descuento debe estar entre 0 y 100 (recibido: ${descuento})`);
}
return precio * (1 - descuento / 100);
}
console.log(precioFinal({ precio: 20, descuento: 15 })); // 17
try {
precioFinal({ precio: '20' });
} catch (error) {
console.log(error.message); // precio debe ser un número positivo (recibido: "20")
}
12.2. Mensajes para el usuario frente a logs
| Mensaje para el usuario | Log para el equipo técnico | |
|---|---|---|
| Destinatario | Persona sin conocimientos técnicos | Desarrolladores |
| Contenido | Qué ha pasado y qué puede hacer | Tipo, mensaje, stack, cause, datos de contexto |
| Tono | Amable, sin jerga | Preciso y completo |
| Ejemplo | “No hemos podido guardar los cambios. Revisa tu conexión e inténtalo de nuevo.” | ErrorRed: POST /api/pedidos 503 (cause: TypeError: Failed to fetch) |
| Datos sensibles | Nunca | Nunca contraseñas ni tokens |
function mensajeParaUsuario(error) {
if (error.name === 'ErrorValidacion') return error.message; // ya es comprensible
if (error.name === 'ErrorRed') return 'Problema de conexión. Inténtalo de nuevo en unos segundos.';
return 'Ha ocurrido un error inesperado. Ya estamos trabajando en ello.';
}
function gestionarError(error, contexto) {
console.error('[ERROR]', contexto, error); // log técnico completo (stack, cause...)
return mensajeParaUsuario(error); // texto para mostrar en pantalla
}
12.3. La consola como herramienta
| Método | Uso |
|---|---|
console.error(...) | Errores: salen en rojo y con la pila en DevTools; en Node van a stderr |
console.warn(...) | Advertencias (amarillo) |
console.table(datos) | Arrays de objetos en forma de tabla |
console.group('título') / console.groupEnd() | Agrupar mensajes relacionados |
console.trace() | Muestra la pila de llamadas actual sin lanzar un error |
console.assert(condición, mensaje) | Muestra un error solo si la condición es falsa |
12.4. Depurar con DevTools
- Abre DevTools (
F12) → pestaña Sources (Chrome/Edge) o Depurador (Firefox). - En el panel Breakpoints, activa Pause on uncaught exceptions (pausar en excepciones no capturadas) y, si lo necesitas, Pause on caught exceptions (también las capturadas).
- Reproduce el error: la ejecución se detiene justo en la línea que lo lanza.
- Inspecciona las variables en Scope, recorre la pila en Call Stack y avanza paso a paso (
F10paso por encima,F11entrar en la función). - También puedes escribir
debugger;en tu código: con DevTools abiertas, la ejecución se pausa en esa línea.
💡 Consejo: “Pausar en excepciones capturadas” es muy útil cuando un
catchdemasiado amplio está escondiendo un error: el depurador se detiene en elthroworiginal antes de que elcatchlo trague.
13. Ejemplo completo: validar un formulario de datos
Validamos los datos de un formulario de registro (como objeto; en el módulo 16 los obtendrás de un formulario real con FormData). Recogemos todos los errores a la vez para mostrarlos juntos, usando errores personalizados y AggregateError.
class ErrorCampo extends Error {
constructor(campo, mensaje, valor) {
super(mensaje);
this.name = 'ErrorCampo';
this.campo = campo;
this.valor = valor;
}
}
class ErrorFormulario extends AggregateError {
constructor(errores) {
super(errores, `El formulario tiene ${errores.length} error(es)`);
this.name = 'ErrorFormulario';
}
get porCampo() {
return Object.fromEntries(this.errors.map((e) => [e.campo, e.message]));
}
}
const reglas = {
nombre: (v) => {
if (typeof v !== 'string' || v.trim().length < 2) throw new ErrorCampo('nombre', 'Escribe al menos 2 caracteres', v);
},
email: (v) => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v ?? '')) throw new ErrorCampo('email', 'El email no tiene un formato válido', v);
},
edad: (v) => {
const n = Number(v);
if (!Number.isInteger(n)) throw new ErrorCampo('edad', 'La edad debe ser un número entero', v);
if (n < 18 || n > 120) throw new ErrorCampo('edad', 'Debes tener entre 18 y 120 años', v);
},
clave: (v) => {
if (typeof v !== 'string' || v.length < 8) throw new ErrorCampo('clave', 'La contraseña necesita al menos 8 caracteres');
},
};
function validarRegistro(datos) {
const errores = [];
for (const [campo, validar] of Object.entries(reglas)) {
try {
validar(datos[campo]);
} catch (error) {
if (!(error instanceof ErrorCampo)) throw error; // un fallo de programación: no lo mezclamos
errores.push(error);
}
}
if (errores.length > 0) throw new ErrorFormulario(errores);
return { ...datos, nombre: datos.nombre.trim(), edad: Number(datos.edad) };
}
function enviarRegistro(datos) {
try {
const limpio = validarRegistro(datos);
console.log(`✅ Registro correcto: ${limpio.nombre} (${limpio.edad} años)`);
} catch (error) {
if (error instanceof ErrorFormulario) {
console.log(`❌ ${error.message}`);
for (const [campo, mensaje] of Object.entries(error.porCampo)) {
console.log(` - ${campo}: ${mensaje}`);
}
} else {
console.error('Error inesperado al registrar', error);
console.log('Ha ocurrido un error inesperado. Inténtalo más tarde.');
}
} finally {
console.log('(botón "Enviar" reactivado)');
}
}
enviarRegistro({ nombre: ' Lucía ', email: 'lucia@correo.es', edad: '29', clave: 'Segura#2026' });
// ✅ Registro correcto: Lucía (29 años)
// (botón "Enviar" reactivado)
enviarRegistro({ nombre: 'L', email: 'lucia@', edad: '15', clave: '123' });
// ❌ El formulario tiene 4 error(es)
// - nombre: Escribe al menos 2 caracteres
// - email: El email no tiene un formato válido
// - edad: Debes tener entre 18 y 120 años
// - clave: La contraseña necesita al menos 8 caracteres
// (botón "Enviar" reactivado)
💡 Consejo: Fíjate en la separación de responsabilidades:
validarRegistrolanza errores con información estructurada (campo, mensaje);enviarRegistrodecide cómo presentarlos. Así puedes reutilizar la validación en el navegador y en un servidor Node sin cambiar nada.
⚠️ Importante: Observa que la regla de
claveno guarda el valor en el error. Nunca incluyas contraseñas ni datos sensibles en los errores: acaban en logs y servicios de monitorización.
14. Ejercicios
Ejercicio 1: Clasificar errores
Escribe cinco fragmentos de código que provoquen, respectivamente, un TypeError, un ReferenceError, un RangeError, un SyntaxError (con JSON.parse) y un URIError. Ejecútalos dentro de un bucle con try/catch (como en la sección 3) y muestra el name de cada uno.
Ejercicio 2: Conversor seguro
Escribe aEntero(texto) que devuelva un número entero o lance TypeError si el argumento no es una cadena y RangeError si la cadena no representa un entero (por ejemplo '12a' o '3.5'). Después escribe aEnteroOPorDefecto(texto, porDefecto) que use la anterior, capture solo el RangeError y relance cualquier otro error.
Ejercicio 3: finally y recursos
Simula un recurso con un objeto conexion = { abierta: false, abrir() {...}, cerrar() {...} }. Escribe consultar(sql) que abra la conexión, lance un error si la consulta contiene 'DROP', y garantice con finally que la conexión se cierra siempre. Comprueba el estado de conexion.abierta tras una consulta correcta y tras una errónea.
Ejercicio 4: Jerarquía de errores HTTP
Crea ErrorHttp (con status y url) y las subclases ErrorNoEncontrado (404), ErrorNoAutorizado (401) y ErrorServidor (500+). Escribe crearErrorHttp(status, url) que devuelva la instancia adecuada y una función mensajeUsuario(error) con textos amables para cada caso.
Ejercicio 5: Cadena de causas
Escribe tres funciones leerFichero, parsearConfig y iniciarApp, donde cada una llama a la anterior y, si falla, relanza un error de más alto nivel con { cause }. Provoca un fallo en la más interna y muestra la cadena completa con la función cadenaDeCausas de la sección 7.
Ejercicio 6: Error.isError en la práctica
Escribe registrar(valorLanzado) que reciba cualquier cosa (un Error, un texto, un número, un objeto) y devuelva siempre un objeto { name, message, stack } normalizado, usando Error.isError. Pruébala con al menos cinco valores distintos, incluido un error creado con vm.runInNewContext en Node.
Ejercicio 7: Validador de pedidos
Amplía el ejemplo de la sección 13 para validar un pedido { cliente, lineas: [{ producto, unidades, precio }], codigoPostal }. Cada línea debe validarse por separado, y el error debe indicar el índice (lineas[2].unidades). Usa ErrorCampo, ErrorFormulario y la validación de código postal del módulo 08.
Ejercicio 8: Red de seguridad en el navegador
Crea una página HTML con tres botones que provoquen un error síncrono, una promesa rechazada sin gestionar y un error dentro de un setTimeout. Registra los manejadores error y unhandledrejection en window para que, en lugar de solo aparecer en la consola, muestren un aviso discreto en la página con un mensaje para el usuario. Activa “Pause on uncaught exceptions” en DevTools y observa dónde se detiene cada uno.
Resumen
| Concepto | Sintaxis / clave |
|---|---|
| Crear un error | new Error(mensaje, { cause }) |
| Propiedades | name, message, stack, cause |
| Tipos nativos | TypeError, ReferenceError, RangeError, SyntaxError, URIError, AggregateError |
| Lanzar | throw new TypeError('...') (siempre un Error) |
| Capturar | try { } catch (error) { } finally { } |
| Sin parámetro | catch { } |
| Relanzar | if (!(error instanceof Esperado)) throw error; |
| Encadenar | throw new Error('alto nivel', { cause: error }) |
| Personalizado | class MiError extends Error { constructor(m, o) { super(m, o); this.name = 'MiError'; } } |
| ¿Es un error? | Error.isError(valor) (ES2026, fiable entre realms) |
| Varios errores | new AggregateError(errores, mensaje), error.errors |
| Asíncrono | .catch() o try + await |
| Global (navegador) | addEventListener('error'), addEventListener('unhandledrejection') |
| Global (Node) | process.on('uncaughtException'), process.on('unhandledRejection') |
| Depurar | console.error, console.trace, debugger;, pausar en excepciones |