Módulo 10: Gestión de errores

Objetivos del módulo

  • Distinguir errores de sintaxis, de ejecución y lógicos
  • Conocer el objeto Error y sus propiedades name, message, stack y cause
  • Reconocer los errores nativos: TypeError, ReferenceError, RangeError, SyntaxError, URIError y AggregateError
  • Lanzar errores con throw y capturarlos con try/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 que instanceof
  • 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 (name o instanceof) 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: pierdes stack, name y la compatibilidad con las herramientas. Lanza siempre instancias de Error o 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: finally se usa para liberar recursos o restaurar estados: cerrar una conexión, ocultar un indicador de “cargando”, desbloquear un botón… Se ejecuta aunque el try o el catch hagan return.

⚠️ Importante: No pongas return dentro de finally: sustituye el valor devuelto por try o catch y, 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 try lo 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 catch vací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 cause automáticamente al imprimir el error con console.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.name evita 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 el name como 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:

  1. Falsos positivos: cualquier objeto cuyo prototipo sea Error.prototype pasa la prueba, aunque no sea un error de verdad.
  2. Falsos negativos entre realms: cada iframe, cada worker y cada contexto node:vm tiene su propio constructor Error. Un error creado en un iframe no es instanceof Error en 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 un iframe tiene dos realms. Es el mismo motivo por el que existe Array.isArray en lugar de confiar en instanceof 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.isError está disponible en Chrome y Edge 134+, Firefox 138+, Safari 18.4+ y Node.js 24.3+. Safari (y Node 24.0-24.2) devuelven false para los DOMException (errores de las APIs del navegador), una limitación conocida. Si necesitas compatibilidad con versiones anteriores, core-js ofrece 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, instanceof sigue 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 await dentro de try o 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 devuelve true, evita el mensaje en consola. Prefiere addEventListener, 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

  1. Abre DevTools (F12) → pestaña Sources (Chrome/Edge) o Depurador (Firefox).
  2. 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).
  3. Reproduce el error: la ejecución se detiene justo en la línea que lo lanza.
  4. Inspecciona las variables en Scope, recorre la pila en Call Stack y avanza paso a paso (F10 paso por encima, F11 entrar en la función).
  5. 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 catch demasiado amplio está escondiendo un error: el depurador se detiene en el throw original antes de que el catch lo 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: validarRegistro lanza errores con información estructurada (campo, mensaje); enviarRegistro decide 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 clave no 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