Express

Express

Tutorial Express: Gestión de errores

Aprende a manejar errores en Express usando middleware de errores y respuestas estructuradas para mejorar la robustez de tus aplicaciones.

Aprende Express y certifícate

Middleware de errores

Express proporciona un mecanismo especializado para manejar errores a través de middleware específicos que se ejecutan cuando ocurre una excepción en la aplicación. A diferencia del middleware regular, el middleware de errores tiene una firma única con cuatro parámetros que permite a Express identificarlo automáticamente.

Definición y estructura básica

El middleware de errores en Express se define con cuatro parámetros obligatorios: err, req, res y next. Esta firma específica es lo que distingue este tipo de middleware del middleware regular:

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Algo salió mal!');
});

Es fundamental que el middleware de errores se registre después de todas las rutas y middleware que puedan generar errores, ya que Express procesa el middleware en el orden en que se define.

Propagación de errores

Para que un error llegue al middleware de errores, debe ser propagado explícitamente usando la función next() con el error como argumento:

app.get('/usuario/:id', (req, res, next) => {
  const userId = req.params.id;
  
  if (!userId || isNaN(userId)) {
    const error = new Error('ID de usuario inválido');
    error.status = 400;
    return next(error); // Propaga el error al middleware de errores
  }
  
  // Simulación de operación que puede fallar
  obtenerUsuario(userId)
    .then(usuario => res.json(usuario))
    .catch(next); // Propaga automáticamente errores de promesas
});

Manejo de errores síncronos y asíncronos

Express captura automáticamente los errores síncronos que ocurren en las rutas, pero los errores asíncronos requieren manejo explícito:

Errores síncronos (capturados automáticamente):

app.get('/error-sincrono', (req, res) => {
  throw new Error('Error síncrono'); // Express lo captura automáticamente
});

Errores asíncronos (requieren manejo explícito):

app.get('/error-asincrono', (req, res, next) => {
  setTimeout(() => {
    try {
      throw new Error('Error asíncrono');
    } catch (err) {
      next(err); // Debe propagarse manualmente
    }
  }, 100);
});

// Con async/await
app.get('/error-async-await', async (req, res, next) => {
  try {
    await operacionAsincrona();
    res.json({ success: true });
  } catch (error) {
    next(error); // Propaga el error
  }
});

Middleware de errores múltiple

Puedes definir múltiples middleware de errores para manejar diferentes tipos de errores o realizar diferentes acciones:

// Middleware para logging de errores
app.use((err, req, res, next) => {
  console.error(`Error en ${req.method} ${req.path}:`, err.message);
  next(err); // Pasa al siguiente middleware de errores
});

// Middleware para errores de validación
app.use((err, req, res, next) => {
  if (err.name === 'ValidationError') {
    return res.status(400).json({
      error: 'Error de validación',
      details: err.message
    });
  }
  next(err); // Si no es error de validación, pasa al siguiente
});

// Middleware general de errores (debe ir al final)
app.use((err, req, res, next) => {
  const status = err.status || 500;
  res.status(status).json({
    error: status === 500 ? 'Error interno del servidor' : err.message
  });
});

Manejo avanzado con información contextual

Un middleware de errores robusto puede incluir información adicional sobre el contexto del error:

app.use((err, req, res, next) => {
  // Información contextual del error
  const errorInfo = {
    timestamp: new Date().toISOString(),
    method: req.method,
    url: req.originalUrl,
    ip: req.ip,
    userAgent: req.get('User-Agent'),
    error: {
      message: err.message,
      stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
    }
  };
  
  // Log del error con contexto
  console.error('Error capturado:', errorInfo);
  
  // Respuesta diferenciada según el entorno
  if (process.env.NODE_ENV === 'production') {
    res.status(err.status || 500).json({
      error: 'Ha ocurrido un error en el servidor'
    });
  } else {
    res.status(err.status || 500).json(errorInfo);
  }
});

Integración con operaciones de base de datos

El middleware de errores es especialmente útil cuando trabajas con operaciones de base de datos que pueden fallar:

app.post('/productos', async (req, res, next) => {
  try {
    const nuevoProducto = await db.productos.create(req.body);
    res.status(201).json(nuevoProducto);
  } catch (error) {
    // Personaliza el error según el tipo
    if (error.code === 'ER_DUP_ENTRY') {
      error.status = 409;
      error.message = 'El producto ya existe';
    }
    next(error);
  }
});

// El middleware de errores maneja todos los errores de DB
app.use((err, req, res, next) => {
  if (err.code && err.code.startsWith('ER_')) {
    return res.status(400).json({
      error: 'Error de base de datos',
      message: err.message
    });
  }
  next(err);
});

El middleware de errores en Express proporciona un punto centralizado para manejar todas las excepciones de tu aplicación, permitiendo un control granular sobre cómo se procesan y responden los diferentes tipos de errores que pueden ocurrir durante la ejecución.

Respuestas de error estructuradas

Una vez que el middleware de errores captura las excepciones, es fundamental devolver respuestas consistentes y bien estructuradas que faciliten tanto la depuración como la integración con clientes frontend. Las respuestas estructuradas proporcionan información clara sobre qué ocurrió, por qué falló la operación y cómo el cliente puede proceder.

Estructura básica de respuestas de error

Una respuesta de error bien estructurada debe incluir información esencial de manera consistente. El formato más común incluye un código de estado HTTP apropiado junto con un cuerpo JSON que describe el error:

app.use((err, req, res, next) => {
  const errorResponse = {
    success: false,
    error: {
      type: err.name || 'Error',
      message: err.message,
      code: err.code || 'INTERNAL_ERROR'
    },
    timestamp: new Date().toISOString(),
    path: req.originalUrl
  };
  
  res.status(err.status || 500).json(errorResponse);
});

Códigos de error personalizados

Implementar un sistema de códigos permite a los clientes identificar tipos específicos de errores sin depender únicamente de los mensajes de texto:

// Definición de códigos de error
const ERROR_CODES = {
  VALIDATION_FAILED: 'VALIDATION_FAILED',
  RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND',
  UNAUTHORIZED_ACCESS: 'UNAUTHORIZED_ACCESS',
  DUPLICATE_RESOURCE: 'DUPLICATE_RESOURCE',
  RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED'
};

// Middleware que asigna códigos según el tipo de error
app.use((err, req, res, next) => {
  let errorCode = ERROR_CODES.INTERNAL_ERROR;
  let statusCode = 500;
  
  // Mapeo de errores a códigos específicos
  if (err.name === 'ValidationError') {
    errorCode = ERROR_CODES.VALIDATION_FAILED;
    statusCode = 400;
  } else if (err.message.includes('not found')) {
    errorCode = ERROR_CODES.RESOURCE_NOT_FOUND;
    statusCode = 404;
  } else if (err.name === 'UnauthorizedError') {
    errorCode = ERROR_CODES.UNAUTHORIZED_ACCESS;
    statusCode = 401;
  }
  
  res.status(statusCode).json({
    success: false,
    error: {
      code: errorCode,
      message: err.message,
      type: err.name
    }
  });
});

Respuestas diferenciadas por entorno

Las respuestas deben adaptarse al entorno de ejecución, proporcionando información detallada en desarrollo pero protegiendo datos sensibles en producción:

app.use((err, req, res, next) => {
  const isDevelopment = process.env.NODE_ENV === 'development';
  
  const baseResponse = {
    success: false,
    error: {
      message: err.message,
      code: err.code || 'INTERNAL_ERROR'
    },
    timestamp: new Date().toISOString()
  };
  
  // Información adicional solo en desarrollo
  if (isDevelopment) {
    baseResponse.error.stack = err.stack;
    baseResponse.error.details = err.details;
    baseResponse.debug = {
      method: req.method,
      url: req.originalUrl,
      headers: req.headers,
      body: req.body
    };
  }
  
  res.status(err.status || 500).json(baseResponse);
});

Manejo de errores de validación

Los errores de validación requieren un formato específico que permita al cliente identificar exactamente qué campos fallaron y por qué:

// Función auxiliar para formatear errores de validación
function formatValidationErrors(validationError) {
  const errors = {};
  
  if (validationError.errors) {
    Object.keys(validationError.errors).forEach(field => {
      errors[field] = {
        message: validationError.errors[field].message,
        value: validationError.errors[field].value,
        kind: validationError.errors[field].kind
      };
    });
  }
  
  return errors;
}

// Middleware específico para errores de validación
app.use((err, req, res, next) => {
  if (err.name === 'ValidationError') {
    return res.status(400).json({
      success: false,
      error: {
        type: 'ValidationError',
        message: 'Los datos proporcionados no son válidos',
        code: 'VALIDATION_FAILED',
        fields: formatValidationErrors(err)
      }
    });
  }
  next(err);
});

Respuestas con sugerencias de acción

Las respuestas estructuradas pueden incluir sugerencias sobre cómo resolver el problema, mejorando la experiencia del desarrollador:

app.use((err, req, res, next) => {
  const errorResponse = {
    success: false,
    error: {
      message: err.message,
      code: err.code || 'INTERNAL_ERROR'
    }
  };
  
  // Agregar sugerencias según el tipo de error
  switch (err.code) {
    case 'RESOURCE_NOT_FOUND':
      errorResponse.suggestion = 'Verifica que el ID del recurso sea correcto';
      break;
    case 'VALIDATION_FAILED':
      errorResponse.suggestion = 'Revisa los campos requeridos y sus formatos';
      break;
    case 'UNAUTHORIZED_ACCESS':
      errorResponse.suggestion = 'Asegúrate de incluir un token de autenticación válido';
      break;
    case 'RATE_LIMIT_EXCEEDED':
      errorResponse.suggestion = 'Espera unos minutos antes de realizar más solicitudes';
      errorResponse.retryAfter = 60; // segundos
      break;
  }
  
  res.status(err.status || 500).json(errorResponse);
});

Localización de mensajes de error

Para aplicaciones multiidioma, las respuestas pueden incluir mensajes localizados basados en las preferencias del cliente:

// Diccionario de mensajes de error
const errorMessages = {
  es: {
    VALIDATION_FAILED: 'Los datos proporcionados no son válidos',
    RESOURCE_NOT_FOUND: 'El recurso solicitado no existe',
    UNAUTHORIZED_ACCESS: 'No tienes permisos para acceder a este recurso'
  },
  en: {
    VALIDATION_FAILED: 'The provided data is not valid',
    RESOURCE_NOT_FOUND: 'The requested resource does not exist',
    UNAUTHORIZED_ACCESS: 'You do not have permission to access this resource'
  }
};

app.use((err, req, res, next) => {
  const language = req.headers['accept-language']?.split(',')[0]?.split('-')[0] || 'en';
  const messages = errorMessages[language] || errorMessages.en;
  
  const errorResponse = {
    success: false,
    error: {
      code: err.code || 'INTERNAL_ERROR',
      message: messages[err.code] || err.message,
      originalMessage: err.message // Mensaje técnico original
    }
  };
  
  res.status(err.status || 500).json(errorResponse);
});

Respuestas con metadatos adicionales

En algunos casos, es útil incluir metadatos que ayuden al cliente a tomar decisiones sobre cómo proceder:

app.use((err, req, res, next) => {
  const errorResponse = {
    success: false,
    error: {
      message: err.message,
      code: err.code || 'INTERNAL_ERROR'
    },
    meta: {
      requestId: req.id || generateRequestId(),
      timestamp: new Date().toISOString(),
      version: process.env.API_VERSION || '1.0.0'
    }
  };
  
  // Metadatos específicos según el tipo de error
  if (err.code === 'RATE_LIMIT_EXCEEDED') {
    errorResponse.meta.retryAfter = err.retryAfter || 60;
    errorResponse.meta.limit = err.limit;
    errorResponse.meta.remaining = 0;
  }
  
  if (err.code === 'VALIDATION_FAILED' && err.fields) {
    errorResponse.meta.invalidFields = Object.keys(err.fields);
  }
  
  res.status(err.status || 500).json(errorResponse);
});

Las respuestas de error estructuradas transforman las excepciones en información útil que permite a los clientes manejar errores de manera inteligente, facilitando tanto la depuración durante el desarrollo como la creación de interfaces de usuario que respondan apropiadamente a diferentes situaciones de error.

Aprende Express online

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.

Accede GRATIS a Express y certifícate

Ejercicios de programación de Express

Evalúa tus conocimientos de esta lección Gestión de errores con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.