El manejo de errores en JavaScript va mucho más allá de envolver código en un try/catch. Crear clases de error propias, encadenar errores con cause, usar AggregateError para recopilar múltiples fallos y gestionar correctamente los errores asíncronos son técnicas que marcan la diferencia en aplicaciones robustas.
Clases de error propias
Crear subclases de Error permite distinguir tipos de errores programáticamente y añadir campos contextuales sin tener que parsear mensajes de texto:
class AppError extends Error {
constructor(mensaje, opciones = {}) {
super(mensaje, opciones);
this.name = this.constructor.name;
// Asegura que el stack trace muestre la clase correcta
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
class ErrorValidacion extends AppError {
constructor(campo, mensaje, valor) {
super(mensaje);
this.campo = campo;
this.valor = valor;
}
}
class ErrorAPI extends AppError {
constructor(mensaje, statusCode, endpoint) {
super(mensaje);
this.statusCode = statusCode;
this.endpoint = endpoint;
}
}
class ErrorBaseDatos extends AppError {
constructor(mensaje, query, opciones) {
super(mensaje, opciones);
this.query = query;
}
}
// Uso
try {
throw new ErrorValidacion('email', 'Formato de email inválido', 'no-es-email');
} catch (err) {
if (err instanceof ErrorValidacion) {
console.log(`Campo '${err.campo}' inválido: ${err.message}`);
console.log(`Valor recibido: ${err.valor}`);
}
}
Encadenamiento de errores con cause (ES2022)
La propiedad cause permite preservar el error original al envolverlo en uno más descriptivo. Esto es clave para mantener el stack trace completo sin perder el contexto del error raíz:
async function obtenerUsuario(id) {
try {
const res = await fetch(`/api/usuarios/${id}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
return await res.json();
} catch (err) {
// Envuelve el error original preservándolo en "cause"
throw new ErrorAPI(
`No se pudo obtener el usuario ${id}`,
err.statusCode ?? 500,
`/api/usuarios/${id}`,
{ cause: err }
);
}
}
try {
await obtenerUsuario(99);
} catch (err) {
console.log(err.message); // "No se pudo obtener el usuario 99"
console.log(err.cause.message); // "HTTP 404: Not Found"
console.log(err instanceof ErrorAPI); // true
// Recorrer la cadena de causas
let actual = err;
while (actual) {
console.log(`- ${actual.name}: ${actual.message}`);
actual = actual.cause;
}
}
AggregateError: recopilar múltiples errores
AggregateError agrupa varios errores en uno solo. Es el tipo que lanza Promise.any() cuando todas las promesas rechazan, pero también es útil para validaciones que comprueban todos los campos antes de fallar:
function validarFormulario(datos) {
const errores = [];
if (!datos.nombre || datos.nombre.trim().length < 2) {
errores.push(new ErrorValidacion('nombre', 'El nombre debe tener al menos 2 caracteres', datos.nombre));
}
if (!/^[^s@]+@[^s@]+.[^s@]+$/.test(datos.email)) {
errores.push(new ErrorValidacion('email', 'Email no válido', datos.email));
}
if (!datos.password || datos.password.length < 8) {
errores.push(new ErrorValidacion('password', 'La contraseña debe tener al menos 8 caracteres', '***'));
}
if (errores.length > 0) {
throw new AggregateError(errores, `El formulario tiene ${errores.length} error(es)`);
}
}
try {
validarFormulario({ nombre: 'A', email: 'no-email', password: '123' });
} catch (err) {
if (err instanceof AggregateError) {
console.log(err.message); // "El formulario tiene 3 error(es)"
err.errors.forEach(e => {
console.log(`${e.campo}: ${e.message}`);
});
}
}
Capturar errores en Promise.any
const endpoints = [
fetch('https://api1.ejemplo.com/datos').then(r => r.json()),
fetch('https://api2.ejemplo.com/datos').then(r => r.json()),
fetch('https://api3.ejemplo.com/datos').then(r => r.json()),
];
try {
const datos = await Promise.any(endpoints);
console.log('Datos obtenidos:', datos);
} catch (err) {
if (err instanceof AggregateError) {
console.log('Todos los endpoints fallaron:');
err.errors.forEach((e, i) => console.log(` ${i + 1}. ${e.message}`));
}
}
Errores asíncronos en Node.js
En Node.js, los errores asíncronos que no se capturan lanzan el evento unhandledRejection. La forma correcta de manejarlos globalmente es escuchar ese evento:
// En Node.js, captura promesas rechazadas sin catch
process.on('unhandledRejection', (reason, promise) => {
console.error('Promesa rechazada sin manejar:', promise);
console.error('Razón:', reason);
// En producción: registrar en sistema de logging y terminar
process.exit(1);
});
// Para excepciones síncronas no capturadas
process.on('uncaughtException', (err) => {
console.error('Excepción no capturada:', err);
process.exit(1);
});
Errores asíncronos en el navegador
// Captura promesas rechazadas sin manejar
window.addEventListener('unhandledrejection', (evento) => {
console.error('Promesa rechazada:', evento.reason);
evento.preventDefault(); // Evita que aparezca en la consola del navegador
// Enviar a sistema de monitorización
enviarAMonitoring({ tipo: 'unhandledRejection', error: evento.reason });
});
// Captura errores síncronos no capturados
window.addEventListener('error', (evento) => {
console.error('Error no capturado:', evento.error);
enviarAMonitoring({ tipo: 'uncaughtError', error: evento.error });
});
Patrón Result: evitar excepciones para flujos de control
Una alternativa a las excepciones para errores esperados es el patrón Result, que devuelve un objeto con ok/error en lugar de lanzar:
async function intentarFetch(url) {
try {
const res = await fetch(url);
if (!res.ok) return { ok: false, error: new ErrorAPI(`HTTP ${res.status}`, res.status, url) };
const datos = await res.json();
return { ok: true, valor: datos };
} catch (err) {
return { ok: false, error: new ErrorAPI('Error de red', 0, url, { cause: err }) };
}
}
const resultado = await intentarFetch('/api/usuario/1');
if (resultado.ok) {
console.log('Usuario:', resultado.valor);
} else {
console.warn('Error:', resultado.error.message);
}
Dominar estas técnicas permite escribir código JavaScript que trata los errores como ciudadanos de primera clase: con tipos propios, contexto preservado, y estrategias de captura adecuadas tanto para el entorno del navegador como de Node.js.
