El problema de relanzar errores sin contexto
En código profesional, un error rara vez ocurre de forma aislada. Un fallo al leer un archivo puede propagarse como error de configuración; un error de red puede manifestarse como fallo de inicialización de un servicio. Durante años, los desarrolladores envolvían errores manualmente, concatenando mensajes y perdiendo el stack trace original en el proceso.
El estándar moderno ofrece Error.cause, una propiedad oficial para enlazar errores en cadena sin perder el origen. Junto con las clases de error personalizadas y AggregateError, forman un sistema de manejo de errores robusto y tipado, apropiado para aplicaciones serias.
// Patron antiguo: se pierde el stack trace original
try {
cargarConfiguracion();
} catch (err) {
throw new Error("No se pudo iniciar: " + err.message);
}
Con el patrón anterior, cuando alguien inspecciona el error final solo ve el mensaje concatenado. El origen exacto del fallo queda oculto tras un string. Las herramientas de observabilidad como Sentry o Datadog tienen que adivinar la causa raíz.
La pérdida del stack trace original es un coste oculto en producción: multiplica el tiempo necesario para diagnosticar incidentes.
Error.cause: encadenar errores preservando contexto
A partir de ES2022, el constructor Error acepta un segundo parámetro con una propiedad cause. La causa puede ser otro Error, o cualquier valor que aporte información sobre el fallo original. El stack trace de la causa se conserva accesible a través de error.cause.
function cargarConfiguracion() {
try {
const contenido = leerArchivo("config.json");
return JSON.parse(contenido);
} catch (err) {
throw new Error("Configuracion invalida o inaccesible", { cause: err });
}
}
try {
cargarConfiguracion();
} catch (err) {
console.error(err.message); // "Configuracion invalida o inaccesible"
console.error(err.cause?.message); // Mensaje del error original
console.error(err.cause?.stack); // Stack trace original completo
}
La cadena se puede repetir a lo largo de varios niveles. Cada capa anade su propio contexto semántico mientras preserva los niveles inferiores. Al imprimir el error en una herramienta moderna, se muestra el stack anidado con mensajes claros de cada nivel.
function iniciarServicio() {
try {
cargarConfiguracion();
} catch (err) {
throw new Error("El servicio no pudo arrancar", { cause: err });
}
}
Recorrer la cadena de causas
Para inspeccionar todos los errores encadenados, un helper simple recorre la propiedad cause hasta que no haya más niveles. Esta función es útil para logging estructurado o para enviar el error completo a un sistema externo.
function cadenaDeErrores(err) {
const niveles = [];
let actual = err;
while (actual) {
niveles.push({
nombre: actual.name,
mensaje: actual.message,
stack: actual.stack
});
actual = actual.cause;
}
return niveles;
}
La regla práctica es envolver con cause cuando cruzas una frontera semántica (capa de red a capa de dominio, por ejemplo) y dejar que el error viaje sin envolver dentro de la misma capa.
Clases de error personalizadas
Lanzar siempre Error genérico obliga al consumidor a distinguir por mensaje de string, algo frágil. Las clases personalizadas que extienden Error permiten distinguir errores por tipo usando instanceof, añadir propiedades específicas del dominio y documentar en el código qué fallos son posibles.
class ErrorValidacion extends Error {
constructor(mensaje, campo, opciones) {
super(mensaje, opciones);
this.name = "ErrorValidacion";
this.campo = campo;
}
}
class ErrorAutenticacion extends Error {
constructor(mensaje, codigo, opciones) {
super(mensaje, opciones);
this.name = "ErrorAutenticacion";
this.codigo = codigo;
}
}
Cada clase anade propiedades propias relevantes. ErrorValidacion lleva el campo que falló; ErrorAutenticacion lleva un código tipo "token_expired" o "credenciales_invalidas". Al capturar, el consumidor puede responder con lógica específica.
function procesarFormulario(datos) {
if (!datos.email) {
throw new ErrorValidacion("Email requerido", "email");
}
if (!datos.password) {
throw new ErrorValidacion("Password requerido", "password");
}
}
try {
procesarFormulario({ email: "ada@ejemplo.com" });
} catch (err) {
if (err instanceof ErrorValidacion) {
console.log(`Campo ${err.campo}: ${err.message}`);
} else if (err instanceof ErrorAutenticacion) {
console.log(`Error ${err.codigo}: ${err.message}`);
} else {
throw err; // Propaga lo que no reconocemos
}
}
Jerarquías de errores
En aplicaciones grandes es común crear una jerarquía de errores: un error base del dominio y subclases específicas. Esto permite atrapar categorías enteras con una sola cláusula catch.
class ErrorDominio extends Error {
constructor(mensaje, opciones) {
super(mensaje, opciones);
this.name = this.constructor.name;
}
}
class ErrorStockInsuficiente extends ErrorDominio {}
class ErrorPagoRechazado extends ErrorDominio {}
class ErrorClienteBaneado extends ErrorDominio {}
try {
procesarPedido(pedido);
} catch (err) {
if (err instanceof ErrorDominio) {
// Registra incidencia de negocio, responde al usuario
notificarCliente(err.message);
} else {
// Error tecnico, relanza o alerta
throw err;
}
}
Separar errores de dominio de errores técnicos (fallos de red, bugs, recursos del sistema) hace que el código de manejo sea mucho más limpio y predecible.
AggregateError para operaciones paralelas
Cuando se ejecutan varias promesas en paralelo con Promise.all, el primer fallo detiene el resto y solo se reporta una causa. Promise.allSettled no falla pero obliga a filtrar manualmente los rechazos. El patrón moderno es Promise.any combinado con AggregateError: si todas las promesas fallan, el resultado es un AggregateError con un array de todos los errores en la propiedad errors.
async function obtenerDesdeEspejos(urls) {
try {
return await Promise.any(urls.map(u => fetch(u)));
} catch (err) {
if (err instanceof AggregateError) {
console.error("Todos los espejos fallaron:");
err.errors.forEach((e, i) => console.error(` ${i}: ${e.message}`));
}
throw err;
}
}
También puedes construir un AggregateError propio cuando has reunido varios fallos y quieres reportarlos juntos. Es habitual en operaciones de validación, migraciones por lotes o sincronización de ficheros.
async function validarLote(elementos) {
const errores = [];
for (const elemento of elementos) {
try {
await validar(elemento);
} catch (err) {
errores.push(err);
}
}
if (errores.length > 0) {
throw new AggregateError(errores, `${errores.length} elementos invalidos`);
}
}
structuredClone para copias profundas
Clonar objetos complejos ha sido un problema recurrente. Las soluciones tradicionales (JSON.parse(JSON.stringify(obj)), Object.assign con spread) fallan con fechas, Map, Set, ArrayBuffer, referencias circulares y otros tipos comunes. La solución moderna es structuredClone(), disponible de forma nativa en navegadores y en Node.js desde la versión 17.
const original = {
nombre: "Ada",
registro: new Date(),
etiquetas: new Set(["admin", "editor"]),
metadatos: new Map([["plan", "pro"]]),
datos: new Uint8Array([1, 2, 3])
};
const copia = structuredClone(original);
console.log(copia.registro instanceof Date); // true
console.log(copia.etiquetas instanceof Set); // true
console.log(copia !== original); // true
Además de tipos complejos, structuredClone soporta referencias circulares sin problemas. El algoritmo replica la estructura completa respetando los vínculos.
const a = { nombre: "padre" };
const b = { nombre: "hijo", padre: a };
a.hijo = b; // Referencia circular
const copia = structuredClone(a);
console.log(copia.hijo.padre === copia); // true, la referencia se preserva
Lo que no clona structuredClone son funciones, prototipos personalizados y algunos tipos como Error (aunque este último se soporta en versiones recientes). Para estos casos siguen siendo necesarias estrategias manuales o librerías específicas.
// Este ejemplo falla
try {
structuredClone({ ejecutar: () => "hola" });
} catch (err) {
console.log(err.message); // Indica que las funciones no son clonables
}
Cuándo usar cada herramienta
El object spread ({ ...obj }) sigue siendo la mejor opción para copias planas de un solo nivel. structuredClone es la elección correcta para copias profundas de objetos con tipos complejos. Las librerías como Lodash solo son necesarias cuando hace falta clonación personalizada (ignorar ciertos campos, transformar valores al clonar) o compatibilidad con entornos muy antiguos.
// Caso 1: copia plana, cambia un campo
const actualizado = { ...usuario, ultimoLogin: new Date() };
// Caso 2: copia profunda completa
const snapshot = structuredClone(estadoAplicacion);
// Caso 3: copia selectiva con logica
const anonimo = { ...usuario, email: "***", telefono: "***" };
Disponer de structuredClone nativo elimina una de las dependencias clásicas de los proyectos JavaScript. Un bundle más pequeño y menos código que mantener.
Combinando Error.cause, clases de error personalizadas, AggregateError y structuredClone, el manejo de errores en JavaScript alcanza un nivel similar al de lenguajes con tipado fuerte: errores tipados, trazabilidad completa, y herramientas nativas para operaciones habituales.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, JavaScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de JavaScript
Explora más contenido relacionado con JavaScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Aplicar Error.cause al relanzar errores preservando la causa original. Definir clases de error personalizadas con propiedades tipadas. Manejar múltiples errores simultáneos con AggregateError. Usar structuredClone para clonar objetos complejos sin librerías externas.