Express
Tutorial Express: Manejo de errores
Aprende a manejar errores asíncronos y SQL en Express usando async/await, try-catch, wrappers y logging para aplicaciones robustas.
Aprende Express y certifícateTry-catch con async/await
El manejo de errores en aplicaciones Express modernas requiere un enfoque específico cuando trabajamos con operaciones asíncronas. Las funciones async/await han revolucionado la forma en que escribimos código asíncrono, pero también introducen nuevos desafíos para el manejo de errores que debemos abordar correctamente.
En Express, cuando una función de middleware o controlador es asíncrona, los errores que se producen dentro de ella no se capturan automáticamente por el sistema de manejo de errores del framework. Esto significa que debemos implementar estrategias específicas para garantizar que todos los errores sean procesados adecuadamente.
Captura básica de errores asincrónicos
La estructura fundamental para manejar errores en funciones asíncronas utiliza bloques try-catch que envuelven el código que puede fallar:
app.get('/usuarios/:id', async (req, res, next) => {
try {
const usuario = await obtenerUsuario(req.params.id);
res.json(usuario);
} catch (error) {
next(error);
}
});
El patrón clave aquí es pasar el error al middleware de manejo de errores de Express mediante next(error)
. Sin esta llamada, Express no sabrá que ha ocurrido un error y la aplicación podría quedarse colgada o comportarse de manera impredecible.
Wrapper para funciones asíncronas
Para evitar repetir el patrón try-catch en cada controlador, podemos crear una función wrapper que automatice este proceso:
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Uso del wrapper
app.get('/productos', asyncHandler(async (req, res) => {
const productos = await obtenerProductos();
res.json(productos);
}));
Esta función wrapper captura automáticamente cualquier promesa rechazada y la pasa al sistema de manejo de errores de Express, eliminando la necesidad de escribir try-catch en cada controlador.
Manejo de errores específicos
En aplicaciones reales, necesitamos manejar diferentes tipos de errores de manera específica. Podemos crear clases de error personalizadas y manejarlas según su tipo:
class ErrorValidacion extends Error {
constructor(mensaje) {
super(mensaje);
this.name = 'ErrorValidacion';
this.statusCode = 400;
}
}
app.post('/usuarios', async (req, res, next) => {
try {
const { email, nombre } = req.body;
if (!email || !nombre) {
throw new ErrorValidacion('Email y nombre son requeridos');
}
const usuario = await crearUsuario({ email, nombre });
res.status(201).json(usuario);
} catch (error) {
if (error instanceof ErrorValidacion) {
return res.status(error.statusCode).json({
error: error.message,
tipo: 'validacion'
});
}
next(error);
}
});
Propagación de errores en cadenas de middleware
Cuando trabajamos con múltiples middlewares asincrónicos, es importante asegurar que los errores se propaguen correctamente a través de toda la cadena:
const validarToken = async (req, res, next) => {
try {
const token = req.headers.authorization?.split(' ')[1];
const usuario = await verificarToken(token);
req.usuario = usuario;
next();
} catch (error) {
next(error);
}
};
const validarPermisos = async (req, res, next) => {
try {
const tienePermiso = await verificarPermisos(req.usuario.id, 'admin');
if (!tienePermiso) {
const error = new Error('Permisos insuficientes');
error.statusCode = 403;
throw error;
}
next();
} catch (error) {
next(error);
}
};
app.delete('/usuarios/:id', validarToken, validarPermisos, async (req, res, next) => {
try {
await eliminarUsuario(req.params.id);
res.status(204).send();
} catch (error) {
next(error);
}
});
Timeout y cancelación de operaciones
Las operaciones asíncronas pueden colgarse indefinidamente, por lo que es recomendable implementar timeouts para evitar que las peticiones se queden esperando:
const conTimeout = (promesa, tiempo) => {
return Promise.race([
promesa,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Operación timeout')), tiempo)
)
]);
};
app.get('/datos-externos', async (req, res, next) => {
try {
const datos = await conTimeout(
fetch('https://api.externa.com/datos'),
5000 // 5 segundos de timeout
);
const resultado = await datos.json();
res.json(resultado);
} catch (error) {
if (error.message === 'Operación timeout') {
error.statusCode = 504;
}
next(error);
}
});
Logging de errores asincrónicos
Es fundamental registrar los errores para facilitar el debugging y monitoreo de la aplicación:
app.get('/reportes', async (req, res, next) => {
try {
const reportes = await generarReportes(req.query);
res.json(reportes);
} catch (error) {
// Logging detallado del error
console.error('Error generando reportes:', {
mensaje: error.message,
stack: error.stack,
usuario: req.usuario?.id,
parametros: req.query,
timestamp: new Date().toISOString()
});
next(error);
}
});
El manejo adecuado de errores con async/await en Express requiere una combinación de técnicas: uso correcto de try-catch, propagación de errores mediante next()
, implementación de wrappers para reducir código repetitivo, y logging apropiado para facilitar el mantenimiento de la aplicación.
Manejo de errores SQL
Las operaciones con bases de datos son una de las fuentes más comunes de errores en aplicaciones Express. Los errores SQL pueden surgir por múltiples razones: conexiones fallidas, consultas malformadas, violaciones de restricciones, o problemas de permisos. Un manejo adecuado de estos errores es crucial para mantener la estabilidad de la aplicación y proporcionar respuestas útiles a los usuarios.
Los errores de base de datos tienen características específicas que los distinguen de otros tipos de errores. Suelen incluir códigos de error específicos del motor de base de datos, información sobre la consulta que falló, y detalles técnicos que pueden ser útiles para el debugging pero no apropiados para mostrar directamente a los usuarios finales.
Identificación de tipos de errores SQL
Cada motor de base de datos utiliza códigos de error específicos para identificar diferentes tipos de problemas. Es importante categorizar estos errores para proporcionar respuestas apropiadas:
const manejarErrorSQL = (error) => {
// MySQL/MariaDB
if (error.code === 'ER_DUP_ENTRY') {
return {
tipo: 'duplicado',
mensaje: 'El registro ya existe',
statusCode: 409
};
}
if (error.code === 'ER_NO_REFERENCED_ROW_2') {
return {
tipo: 'referencia',
mensaje: 'Referencia a registro inexistente',
statusCode: 400
};
}
// PostgreSQL
if (error.code === '23505') {
return {
tipo: 'duplicado',
mensaje: 'Violación de restricción única',
statusCode: 409
};
}
if (error.code === '23503') {
return {
tipo: 'referencia',
mensaje: 'Violación de clave foránea',
statusCode: 400
};
}
// Error genérico
return {
tipo: 'sql',
mensaje: 'Error en la operación de base de datos',
statusCode: 500
};
};
Manejo de errores en operaciones CRUD
Las operaciones básicas de base de datos requieren estrategias específicas de manejo de errores según el tipo de operación:
// Crear usuario con manejo de duplicados
app.post('/usuarios', async (req, res, next) => {
try {
const { email, nombre } = req.body;
const usuario = await db.query(
'INSERT INTO usuarios (email, nombre) VALUES (?, ?)',
[email, nombre]
);
res.status(201).json({
id: usuario.insertId,
email,
nombre
});
} catch (error) {
const errorInfo = manejarErrorSQL(error);
if (errorInfo.tipo === 'duplicado') {
return res.status(errorInfo.statusCode).json({
error: 'El email ya está registrado',
campo: 'email'
});
}
next(error);
}
});
// Actualizar con validación de existencia
app.put('/usuarios/:id', async (req, res, next) => {
try {
const { id } = req.params;
const { nombre, email } = req.body;
const resultado = await db.query(
'UPDATE usuarios SET nombre = ?, email = ? WHERE id = ?',
[nombre, email, id]
);
if (resultado.affectedRows === 0) {
return res.status(404).json({
error: 'Usuario no encontrado'
});
}
res.json({ mensaje: 'Usuario actualizado correctamente' });
} catch (error) {
const errorInfo = manejarErrorSQL(error);
if (errorInfo.tipo === 'duplicado') {
return res.status(errorInfo.statusCode).json({
error: 'El email ya está en uso por otro usuario',
campo: 'email'
});
}
next(error);
}
});
Transacciones y rollback automático
Las transacciones requieren un manejo especial de errores para garantizar la consistencia de los datos:
app.post('/pedidos', async (req, res, next) => {
const conexion = await db.getConnection();
try {
await conexion.beginTransaction();
// Crear el pedido
const pedido = await conexion.query(
'INSERT INTO pedidos (usuario_id, total) VALUES (?, ?)',
[req.body.usuario_id, req.body.total]
);
// Agregar productos al pedido
for (const producto of req.body.productos) {
await conexion.query(
'INSERT INTO pedido_productos (pedido_id, producto_id, cantidad, precio) VALUES (?, ?, ?, ?)',
[pedido.insertId, producto.id, producto.cantidad, producto.precio]
);
// Actualizar stock
const stockActual = await conexion.query(
'SELECT stock FROM productos WHERE id = ?',
[producto.id]
);
if (stockActual[0].stock < producto.cantidad) {
throw new Error(`Stock insuficiente para el producto ${producto.id}`);
}
await conexion.query(
'UPDATE productos SET stock = stock - ? WHERE id = ?',
[producto.cantidad, producto.id]
);
}
await conexion.commit();
res.status(201).json({
pedido_id: pedido.insertId,
mensaje: 'Pedido creado correctamente'
});
} catch (error) {
await conexion.rollback();
if (error.message.includes('Stock insuficiente')) {
return res.status(400).json({
error: error.message,
tipo: 'stock'
});
}
const errorInfo = manejarErrorSQL(error);
if (errorInfo.tipo === 'referencia') {
return res.status(400).json({
error: 'Usuario o producto no válido'
});
}
next(error);
} finally {
conexion.release();
}
});
Validación de consultas dinámicas
Las consultas construidas dinámicamente son especialmente propensas a errores y requieren validación adicional:
app.get('/productos/buscar', async (req, res, next) => {
try {
const { categoria, precio_min, precio_max, ordenar } = req.query;
let consulta = 'SELECT * FROM productos WHERE 1=1';
const parametros = [];
if (categoria) {
consulta += ' AND categoria = ?';
parametros.push(categoria);
}
if (precio_min) {
if (isNaN(precio_min)) {
return res.status(400).json({
error: 'precio_min debe ser un número válido'
});
}
consulta += ' AND precio >= ?';
parametros.push(parseFloat(precio_min));
}
if (precio_max) {
if (isNaN(precio_max)) {
return res.status(400).json({
error: 'precio_max debe ser un número válido'
});
}
consulta += ' AND precio <= ?';
parametros.push(parseFloat(precio_max));
}
// Validar campo de ordenamiento
const camposValidos = ['nombre', 'precio', 'fecha_creacion'];
if (ordenar && !camposValidos.includes(ordenar)) {
return res.status(400).json({
error: 'Campo de ordenamiento no válido'
});
}
if (ordenar) {
consulta += ` ORDER BY ${ordenar}`;
}
const productos = await db.query(consulta, parametros);
res.json(productos);
} catch (error) {
// Log del error con contexto
console.error('Error en búsqueda de productos:', {
consulta: req.query,
error: error.message,
timestamp: new Date().toISOString()
});
next(error);
}
});
Manejo de errores de conexión
Los problemas de conectividad con la base de datos requieren estrategias específicas de recuperación:
const ejecutarConReintentos = async (operacion, maxReintentos = 3) => {
for (let intento = 1; intento <= maxReintentos; intento++) {
try {
return await operacion();
} catch (error) {
// Errores de conexión que pueden ser temporales
if (error.code === 'ECONNRESET' ||
error.code === 'ETIMEDOUT' ||
error.code === 'ENOTFOUND') {
if (intento === maxReintentos) {
throw new Error('Base de datos no disponible después de varios intentos');
}
// Esperar antes del siguiente intento
await new Promise(resolve => setTimeout(resolve, 1000 * intento));
continue;
}
// Para otros errores, no reintentar
throw error;
}
}
};
app.get('/estadisticas', async (req, res, next) => {
try {
const estadisticas = await ejecutarConReintentos(async () => {
return await db.query(`
SELECT
COUNT(*) as total_usuarios,
COUNT(CASE WHEN activo = 1 THEN 1 END) as usuarios_activos,
AVG(edad) as edad_promedio
FROM usuarios
`);
});
res.json(estadisticas[0]);
} catch (error) {
if (error.message.includes('Base de datos no disponible')) {
return res.status(503).json({
error: 'Servicio temporalmente no disponible',
codigo: 'DB_UNAVAILABLE'
});
}
next(error);
}
});
Logging específico para errores SQL
El registro detallado de errores SQL facilita el debugging y el monitoreo de la aplicación:
const logErrorSQL = (error, contexto) => {
const logData = {
timestamp: new Date().toISOString(),
tipo: 'ERROR_SQL',
codigo: error.code,
mensaje: error.message,
consulta: error.sql,
parametros: contexto.parametros,
usuario: contexto.usuario_id,
endpoint: contexto.endpoint,
stack: error.stack
};
// En producción, usar un sistema de logging apropiado
console.error('Error SQL:', JSON.stringify(logData, null, 2));
// Opcional: enviar a servicio de monitoreo
// monitoreo.reportarError(logData);
};
app.delete('/productos/:id', async (req, res, next) => {
try {
const { id } = req.params;
const resultado = await db.query(
'DELETE FROM productos WHERE id = ?',
[id]
);
if (resultado.affectedRows === 0) {
return res.status(404).json({
error: 'Producto no encontrado'
});
}
res.status(204).send();
} catch (error) {
logErrorSQL(error, {
parametros: [req.params.id],
usuario_id: req.usuario?.id,
endpoint: req.originalUrl
});
const errorInfo = manejarErrorSQL(error);
if (errorInfo.tipo === 'referencia') {
return res.status(400).json({
error: 'No se puede eliminar: el producto está siendo utilizado'
});
}
next(error);
}
});
El manejo efectivo de errores SQL en Express requiere identificar patrones específicos de cada motor de base de datos, implementar estrategias de recuperación para errores temporales, y proporcionar respuestas útiles sin exponer información sensible del sistema.
Otras lecciones de Express
Accede a todas las lecciones de Express y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Expressjs
Introducción Y Entorno
Instalación De Express
Introducción Y Entorno
Estados Http
Routing
Métodos Delete
Routing
Parámetros Y Query Strings
Routing
Métodos Get
Routing
Ejercicios de programación de Express
Evalúa tus conocimientos de esta lección Manejo de errores con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.