Manejo de errores y respuestas consistentes en Node.js con Express

Capítulo 7

Tiempo estimado de lectura: 7 minutos

+ Ejercicio

Por qué necesitas un manejo de errores consistente

En una API con Express, los errores pueden ocurrir por validaciones (entrada inválida), recursos inexistentes, conflictos de negocio (duplicados) o fallos inesperados. Si cada ruta responde distinto, el cliente no puede manejar bien los fallos. La idea es implementar un flujo único: capturar errores síncronos y asíncronos, centralizarlos en un middleware de error y responder siempre con un formato estable.

Formato de respuesta de error

Usaremos un esquema simple y predecible:

{ "message": "...", "code": "...", "details": { ... } }
  • message: texto legible para el cliente (sin detalles internos).
  • code: identificador estable para lógica del frontend (por ejemplo VALIDATION_ERROR).
  • details: información opcional (por ejemplo campos inválidos). Debe ser segura para exponer.

Paso a paso: flujo robusto de errores en Express

Paso 1: crear una clase de error personalizada

Necesitas una forma uniforme de “lanzar” errores con metadatos (código HTTP, código lógico y detalles). Crea un archivo errors/AppError.js:

class AppError extends Error {  constructor({ message, code, status, details }) {    super(message);    this.name = "AppError";    this.code = code || "INTERNAL_ERROR";    this.status = status || 500;    this.details = details;  }}module.exports = { AppError };

Este error será el que uses en tu código de negocio para expresar fallos esperados (400, 404, 409, etc.).

Paso 2: helpers para crear errores comunes

Para evitar repetir valores, crea un archivo errors/httpErrors.js:

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

const { AppError } = require("./AppError");const badRequest = (message, details) =>  new AppError({ message, code: "BAD_REQUEST", status: 400, details });const notFound = (message, details) =>  new AppError({ message, code: "NOT_FOUND", status: 404, details });const conflict = (message, details) =>  new AppError({ message, code: "CONFLICT", status: 409, details });module.exports = { badRequest, notFound, conflict };

Así, en tus rutas o servicios podrás hacer throw notFound(...) o throw badRequest(...) de forma clara.

Paso 3: manejar errores síncronos vs asíncronos

En Express, un error síncrono lanzado dentro de un handler suele ser capturado automáticamente y enviado al middleware de error. El problema principal aparece con handlers async: si lanzas un error o falla una promesa, debes asegurarte de que llegue a next(err).

Paso 4: wrapper para handlers async (evitar try/catch repetitivo)

Crea un helper utils/asyncHandler.js para envolver handlers asíncronos:

const asyncHandler = (fn) => (req, res, next) => {  Promise.resolve(fn(req, res, next)).catch(next);};module.exports = { asyncHandler };

Con esto, cualquier throw dentro de un handler async o cualquier rechazo de promesa irá al middleware de error sin escribir try/catch en cada ruta.

Paso 5: middleware centralizado de error

Este middleware será el único lugar donde se decide el HTTP status y el formato de respuesta. Crea middlewares/errorHandler.js:

const { AppError } = require("../errors/AppError");const errorHandler = (err, req, res, next) => {  const isAppError = err instanceof AppError;  const status = isAppError ? err.status : 500;  const code = isAppError ? err.code : "INTERNAL_ERROR";  const message = isAppError ? err.message : "Ocurrió un error inesperado";  const details = isAppError ? err.details : undefined;  if (!isAppError) {    console.error("[UNHANDLED_ERROR]", err);  }  const payload = { message, code };  if (details !== undefined) payload.details = details;  res.status(status).json(payload);};module.exports = { errorHandler };

Observa dos decisiones importantes:

  • Si el error no es AppError, respondemos con un mensaje genérico para no filtrar detalles internos.
  • El error real se registra en servidor (por ejemplo con console.error o un logger), pero no se expone al cliente.

Paso 6: conectar el middleware de error al final

En tu archivo principal (por ejemplo app.js), registra el middleware de error después de tus rutas:

const express = require("express");const { errorHandler } = require("./middlewares/errorHandler");const app = express();app.use(express.json());// ... aquí van tus rutas// Middleware de error al finalapp.use(errorHandler);module.exports = { app };

El orden importa: si lo pones antes de las rutas, no recibirá los errores generados por ellas.

Mapeo de tipos de error a códigos HTTP (400, 404, 409, 500)

Una regla práctica es separar “errores esperados” (por entrada o negocio) de “errores inesperados” (bugs, caídas de servicios, etc.). Con AppError y los helpers, el mapeo queda explícito:

SituaciónHTTPcodeEjemplo
Entrada inválida (validación)400BAD_REQUEST / VALIDATION_ERRORFalta un campo requerido
Recurso inexistente404NOT_FOUNDID no existe
Conflicto de negocio409CONFLICTEmail ya registrado
Fallo inesperado500INTERNAL_ERRORExcepción no controlada

Si quieres distinguir validación de otros 400, puedes crear un helper adicional:

const validationError = (details) =>  new AppError({    message: "Datos inválidos",    code: "VALIDATION_ERROR",    status: 400,    details  });

Ejemplo práctico: rutas con errores consistentes

Supón un recurso users. Implementaremos tres casos: validación (400), no encontrado (404) y conflicto (409). En un archivo routes/users.js:

const express = require("express");const router = express.Router();const { asyncHandler } = require("../utils/asyncHandler");const { badRequest, notFound, conflict } = require("../errors/httpErrors");// Simulación de persistencia en memoriaconst users = new Map();router.post("/users", asyncHandler(async (req, res) => {  const { email, name } = req.body;  if (!email || !name) {    throw badRequest("Faltan campos requeridos", { required: ["email", "name"] });  }  if (users.has(email)) {    throw conflict("El usuario ya existe", { field: "email" });  }  const user = { email, name };  users.set(email, user);  res.status(201).json({ data: user });}));router.get("/users/:email", asyncHandler(async (req, res) => {  const { email } = req.params;  const user = users.get(email);  if (!user) {    throw notFound("Usuario no encontrado", { email });  }  res.json({ data: user });}));module.exports = { router };

Notas:

  • Los errores se lanzan con throw y el wrapper asyncHandler los envía al middleware centralizado.
  • Las respuestas exitosas pueden tener su propio formato (por ejemplo { data: ... }), pero lo importante aquí es que los errores siempre respetan { message, code, details }.

Conectar estas rutas

En tu archivo de app:

const express = require("express");const { router: usersRouter } = require("./routes/users");const { errorHandler } = require("./middlewares/errorHandler");const app = express();app.use(express.json());app.use(usersRouter);app.use(errorHandler);module.exports = { app };

try/catch en handlers async: cuándo usarlo

Con asyncHandler, no necesitas try/catch para “reenviar” errores. Sin embargo, sí es útil cuando quieres transformar un error de bajo nivel en un error de dominio seguro.

Ejemplo: una función que podría lanzar un error interno (simulado) y tú quieres devolver 409 o 400 según el caso:

const { AppError } = require("../errors/AppError");const { conflict } = require("../errors/httpErrors");async function createUserInDb(input) {  // Simula un error de "duplicado" desde una capa inferior  const err = new Error("duplicate key");  err.code = "DUPLICATE_KEY";  throw err;}router.post("/users-db", asyncHandler(async (req, res) => {  try {    const user = await createUserInDb(req.body);    res.status(201).json({ data: user });  } catch (err) {    if (err.code === "DUPLICATE_KEY") {      throw conflict("El usuario ya existe", { field: "email" });    }    // Si no sabes qué es, re-lanza para que sea 500 genérico    throw err;  }}));

La clave es: el try/catch no es para responder ahí mismo, sino para convertir errores a AppError cuando tiene sentido.

Evitar filtrar detalles internos al cliente

Un error inesperado puede contener stack traces, rutas del sistema, queries o datos sensibles. Para evitar filtraciones:

  • Responde con un mensaje genérico cuando no sea un AppError.
  • No incluyas err.stack ni el mensaje original en la respuesta.
  • En details, incluye solo datos que el cliente pueda usar sin comprometer seguridad (por ejemplo campos inválidos, no valores sensibles).

Si necesitas depurar, registra el error completo en servidor. El cliente se guía por code y message, y opcionalmente por details.

Comprobación rápida con ejemplos de respuesta

Error 400 (validación)

// HTTP 400{ "message": "Faltan campos requeridos", "code": "BAD_REQUEST", "details": { "required": ["email", "name"] } }

Error 404 (no encontrado)

// HTTP 404{ "message": "Usuario no encontrado", "code": "NOT_FOUND", "details": { "email": "test@example.com" } }

Error 409 (conflicto)

// HTTP 409{ "message": "El usuario ya existe", "code": "CONFLICT", "details": { "field": "email" } }

Error 500 (inesperado)

// HTTP 500{ "message": "Ocurrió un error inesperado", "code": "INTERNAL_ERROR" }

Ahora responde el ejercicio sobre el contenido:

¿Cuál es la razón principal para usar un middleware centralizado de errores y un formato de respuesta estable en una API con Express?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Un flujo centralizado permite que todas las rutas respondan con el mismo formato (message, code, details) y con estados HTTP coherentes. Además, ayuda a no filtrar detalles internos cuando el error no es esperado.

Siguiente capítulo

Configuración con variables de entorno en aplicaciones Node.js

Arrow Right Icon
Portada de libro electrónico gratuitaNode.js para principiantes: crea un backend simple con Express
58%

Node.js para principiantes: crea un backend simple con Express

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.