Manejo de errores con catch
Introducción
Cuando trabajamos con promesas en JavaScript, es fundamental saber cómo responder cuando las cosas no salen según lo esperado. El método catch()
es una herramienta esencial que nos permite detectar, gestionar y recuperarnos de errores que pueden ocurrir durante la ejecución de operaciones asíncronas. Un manejo de errores adecuado no sólo mejora la experiencia del usuario al evitar que la aplicación se bloquee, sino que también facilita la depuración y el mantenimiento del código.
En este artículo, exploraremos en profundidad cómo utilizar el método catch()
para gestionar errores en nuestras promesas, cómo implementar estrategias efectivas de manejo de errores y cómo construir aplicaciones más robustas y resistentes a fallos.
El método catch() en promesas
El método catch()
es parte fundamental de la API de Promesas y nos permite capturar errores que ocurren durante la ejecución de una promesa. Técnicamente, catch(onRejected)
es un alias para then(undefined, onRejected)
, lo que significa que se especializa en manejar únicamente los casos de rechazo.
Veamos la sintaxis básica:
miPromesa
.then(resultado => {
// Código que se ejecuta cuando la promesa se resuelve correctamente
console.log('Operación exitosa:', resultado);
})
.catch(error => {
// Código que se ejecuta cuando la promesa es rechazada
console.error('Ocurrió un error:', error);
});
La función que pasamos a catch()
recibe como parámetro el motivo del rechazo, que generalmente es un objeto Error o cualquier valor que se haya pasado al método reject()
de la promesa.
Captura de errores en diferentes niveles
Una característica poderosa del método catch()
es que puede capturar errores que ocurren en cualquier punto de la cadena de promesas anterior a él. Esto significa que podemos tener múltiples operaciones then()
y un único catch()
al final para manejar cualquier error.
obtenerDatos()
.then(datos => procesarDatos(datos))
.then(datosFormateados => mostrarResultados(datosFormateados))
.catch(error => {
// Este catch captura errores de cualquiera de las promesas anteriores
console.error('Error en la cadena de operaciones:', error);
});
También podemos tener múltiples bloques catch()
en diferentes puntos de la cadena para manejar errores específicos:
obtenerDatos()
.then(datos => {
if (!datos) {
throw new Error('No se recibieron datos');
}
return procesarDatos(datos);
})
.catch(error => {
// Este catch sólo maneja errores hasta este punto
console.error('Error al obtener o validar datos:', error);
// Devolvemos datos por defecto para continuar la cadena
return { tipo: 'datos_por_defecto', contenido: [] };
})
.then(datosFormateados => mostrarResultados(datosFormateados))
.catch(error => {
// Este catch maneja errores en mostrarResultados
console.error('Error al mostrar resultados:', error);
});
Propagación de errores en cadenas
Cuando trabajamos con cadenas de promesas, los errores se propagan automáticamente hacia abajo hasta encontrar un catch()
que los maneje. Si un then()
devuelve una promesa rechazada, esa promesa rechazada se propagará a través de la cadena.
Promise.resolve(1)
.then(valor => {
console.log('Primer then:', valor); // 1
// Lanzamos un error
throw new Error('Error en el primer then');
// Este return nunca se ejecuta
return valor + 1;
})
.then(valor => {
// Este bloque nunca se ejecuta porque el error se propaga
console.log('Segundo then:', valor);
return valor + 1;
})
.then(valor => {
// Este bloque tampoco se ejecuta
console.log('Tercer then:', valor);
return valor + 1;
})
.catch(error => {
// Aquí se captura el error
console.error('Error capturado:', error.message); // "Error en el primer then"
return 'Recuperado'; // Devolvemos un valor para continuar la cadena
})
.then(valor => {
// Este bloque sí se ejecuta porque el catch devolvió un valor
console.log('Después del catch:', valor); // "Recuperado"
});
En este ejemplo, una vez que se lanza el error en el primer then()
, los siguientes bloques then()
se omiten hasta que el error es capturado por el catch()
. Después del catch()
, la cadena continúa normalmente.
Recuperación de errores
Una de las grandes ventajas del método catch()
es que nos permite recuperarnos de errores y seguir con la ejecución de la cadena. Podemos hacer esto devolviendo un valor desde el manejador de catch()
:
function obtenerDatosDeAPI() {
return new Promise((resolve, reject) => {
const exito = Math.random() > 0.5; // Simulamos éxito o fracaso aleatorio
setTimeout(() => {
if (exito) {
resolve({ id: 123, nombre: 'Producto ejemplo', precio: 29.99 });
} else {
reject(new Error('No se pudo conectar con el servidor'));
}
}, 1000);
});
}
obtenerDatosDeAPI()
.then(datos => {
console.log('Datos recibidos:', datos);
return datos;
})
.catch(error => {
console.error('Error al obtener datos:', error.message);
console.log('Usando datos de respaldo...');
// Devolvemos datos de respaldo para continuar
return { id: 0, nombre: 'Producto de respaldo', precio: 0 };
})
.then(datos => {
// Este código se ejecuta tanto con los datos reales como con los de respaldo
console.log(`Mostrando producto: ${datos.nombre} - ${datos.precio}€`);
});
En este ejemplo, si la promesa obtenerDatosDeAPI()
es rechazada, el catch()
proporciona datos de respaldo, permitiendo que la aplicación continúe funcionando en lugar de fallar por completo.
Lanzamiento de nuevos errores
Dentro de un bloque catch()
, podemos decidir lanzar un nuevo error para indicar que no pudimos manejar adecuadamente el error original o para transformar el error en uno más específico:
obtenerUsuario(id)
.then(usuario => {
if (!usuario) {
throw new Error('Usuario no encontrado');
}
return obtenerPermisos(usuario.id);
})
.catch(error => {
if (error.message === 'Usuario no encontrado') {
// Registramos el error y lanzamos uno nuevo más específico
console.warn('Advertencia:', error.message);
throw new Error('Error de acceso: el usuario solicitado no existe');
} else if (error.name === 'NetworkError') {
// Intentamos una estrategia alternativa
console.warn('Problema de red, usando caché local');
return obtenerPermisosDesdeCache(id);
} else {
// Para otros errores, los propagamos hacia abajo
throw error;
}
})
.then(permisos => {
// Procesar permisos
})
.catch(error => {
// Este catch capturará tanto el nuevo error lanzado como cualquier otro
console.error('Error final:', error.message);
});
Este patrón es útil para implementar una estrategia de manejo de errores por niveles, donde diferentes partes del código pueden manejar diferentes tipos de errores.
Errores silenciosos y cómo evitarlos
Un error común al trabajar con promesas es olvidar añadir un manejador catch()
, lo que puede resultar en "errores silenciosos" - errores que ocurren pero no son capturados ni manejados adecuadamente:
// ❌ MAL: Sin gestión de errores
function cargarDatos() {
obtenerDatosDeAPI()
.then(datos => {
procesarDatos(datos);
});
// Si ocurre un error, nunca lo sabremos
}
// ✅ BIEN: Con gestión de errores
function cargarDatos() {
obtenerDatosDeAPI()
.then(datos => {
procesarDatos(datos);
})
.catch(error => {
console.error('Error al cargar datos:', error);
mostrarMensajeDeError('No se pudieron cargar los datos');
});
}
Para evitar los errores silenciosos:
- Siempre añade un
catch()
al final de tus cadenas de promesas - Utiliza un manejador global de errores de promesas no capturadas
// Manejador global para promesas no capturadas (solo para depuración)
window.addEventListener('unhandledrejection', event => {
console.error('Error de promesa no capturado:', event.reason);
});
Patrones robustos de manejo de errores
Patrón 1: Clasificación de errores
Crear clases de error personalizadas para diferentes tipos de errores facilita su manejo:
// Definimos clases de error personalizadas
class ErrorDeRed extends Error {
constructor(mensaje) {
super(mensaje);
this.name = 'ErrorDeRed';
}
}
class ErrorDeAutorizacion extends Error {
constructor(mensaje) {
super(mensaje);
this.name = 'ErrorDeAutorizacion';
}
}
class ErrorDeDatos extends Error {
constructor(mensaje) {
super(mensaje);
this.name = 'ErrorDeDatos';
}
}
// Función que lanza diferentes tipos de errores
function obtenerRecurso(id) {
return new Promise((resolve, reject) => {
const aleatorio = Math.random();
setTimeout(() => {
if (aleatorio < 0.3) {
reject(new ErrorDeRed('No se pudo conectar al servidor'));
} else if (aleatorio < 0.6) {
reject(new ErrorDeAutorizacion('No tienes permisos para este recurso'));
} else if (aleatorio < 0.9) {
reject(new ErrorDeDatos('El recurso está corrupto o tiene formato inválido'));
} else {
resolve({id: id, nombre: 'Recurso ' + id});
}
}, 1000);
});
}
// Manejo específico según el tipo de error
obtenerRecurso(123)
.then(recurso => {
console.log('Recurso obtenido:', recurso);
})
.catch(error => {
if (error instanceof ErrorDeRed) {
console.error('Problema de conexión:', error.message);
mostrarMensajeReconexion();
} else if (error instanceof ErrorDeAutorizacion) {
console.error('Problema de permisos:', error.message);
redirigirALogin();
} else if (error instanceof ErrorDeDatos) {
console.error('Problema con los datos:', error.message);
intentarRepararDatos();
} else {
console.error('Error desconocido:', error);
mostrarErrorGenerico();
}
});
Patrón 2: Reintentos automáticos
Para operaciones que pueden fallar temporalmente, como solicitudes de red:
function obtenerDatosConReintentos(url, maxReintentos = 3, retrasoBase = 1000) {
return new Promise((resolve, reject) => {
function intentar(intento) {
fetch(url)
.then(respuesta => {
if (!respuesta.ok) {
throw new Error(`HTTP error! status: ${respuesta.status}`);
}
return respuesta.json();
})
.then(datos => {
resolve(datos); // Éxito, resolvemos la promesa
})
.catch(error => {
console.warn(`Intento ${intento} fallido:`, error.message);
if (intento < maxReintentos) {
// Calculamos un retraso con backoff exponencial
const retraso = retrasoBase * Math.pow(2, intento - 1);
console.log(`Reintentando en ${retraso}ms...`);
setTimeout(() => {
intentar(intento + 1);
}, retraso);
} else {
// Si superamos el número máximo de reintentos, rechazamos la promesa
reject(new Error(`Fallaron los ${maxReintentos} intentos. Último error: ${error.message}`));
}
});
}
// Comenzamos con el primer intento
intentar(1);
});
}
// Uso
obtenerDatosConReintentos('https://api.ejemplo.com/datos', 3)
.then(datos => {
console.log('Datos obtenidos exitosamente:', datos);
})
.catch(error => {
console.error('Error final después de reintentos:', error.message);
mostrarErrorAlUsuario('No se pudieron cargar los datos. Por favor, inténtalo más tarde.');
});
Patrón 3: Envoltura de catch para depuración
Para facilitar la depuración sin perder la funcionalidad de recuperación:
function catchConLog(fn) {
return function(...args) {
return fn(...args)
.catch(error => {
console.error('Error en función:', fn.name, error);
throw error; // Re-lanzamos el error para mantener la cadena de rechazo
});
};
}
// Aplicamos la envoltura a nuestras funciones
const obtenerUsuarioSeguro = catchConLog(obtenerUsuario);
const obtenerPermisosSeguro = catchConLog(obtenerPermisos);
// Uso
obtenerUsuarioSeguro(123)
.then(usuario => obtenerPermisosSeguro(usuario.id))
.then(permisos => {
console.log('Operación completada con éxito');
})
.catch(error => {
// Este catch seguirá recibiendo el error, pero ya habremos visto los logs detallados
console.error('Error general:', error.message);
});
Mejores prácticas con promesas
-
Sé específico con los errores:
- Utiliza mensajes de error descriptivos
- Incluye información relevante (ID, valores, etc.)
- Considera usar clases de error personalizadas
-
Maneja los errores lo más cerca posible de su origen:
- Captura y maneja errores donde tengas el contexto para hacerlo
- Transforma errores específicos en errores más generales cuando sea apropiado
-
Evita catch vacíos:
// ❌ MAL: Catch vacío que oculta errores promesa.catch(() => {}); // ✅ BIEN: Al menos registra el error promesa.catch(error => { console.error('Error en operación:', error); });
-
Encadena correctamente:
- Asegúrate de devolver valores o promesas desde los manejadores
then()
ycatch()
- Mantén la cadena fluida evitando anidaciones innecesarias
- Asegúrate de devolver valores o promesas desde los manejadores
-
Maneja tanto los casos de éxito como los de error:
promesa .then(resultado => { // Manejar éxito }) .catch(error => { // Manejar error }) .finally(() => { // Código que se ejecuta en ambos casos ocultarIndicadorCarga(); });
-
Proporciona valores por defecto sensatos:
obtenerConfiguracion() .catch(error => { console.warn('No se pudo cargar la configuración:', error); return CONFIGURACION_POR_DEFECTO; }) .then(config => { iniciarAplicacion(config); });
-
Considera el contexto del usuario:
- Transforma errores técnicos en mensajes amigables para el usuario
- Ofrece acciones concretas que el usuario pueda realizar para resolver el problema
Resumen
El método catch()
es una herramienta fundamental en el manejo de errores con promesas. Nos permite:
- Capturar errores en diferentes niveles de una cadena de promesas
- Implementar estrategias de recuperación para mantener la aplicación funcionando
- Transformar errores técnicos en mensajes comprensibles para el usuario
- Construir aplicaciones más robustas y resistentes a fallos
Un buen manejo de errores es lo que distingue a una aplicación profesional de una amateur. Mediante el uso adecuado de catch()
y la implementación de patrones robustos de manejo de errores, podemos crear experiencias que funcionan correctamente incluso cuando las cosas no salen según lo planeado.
Recuerda que los errores son inevitables, pero una aplicación que maneja los errores con elegancia proporciona una experiencia de usuario mucho mejor que una que simplemente falla.