Ir al contenido
angel.library

Diseño de respuestas de error

El manejo de errores centralizado resuelve dónde se atrapan los errores — esta guía es sobre qué forma tiene la respuesta que el cliente recibe, para que sea consistente en toda la API.

// Mal: cada endpoint devuelve una forma distinta
res.status(400).json({ error: "Falta el email" })
res.status(400).json("Email inválido")
res.status(400).json({ message: "Bad request", field: "email" })
// Bien: un solo formato, en todos lados
res.status(400).json({
error: {
code: "VALIDATION_ERROR",
message: "Email inválido"
}
})

Un cliente (frontend, app mobile, otro servicio) que consume la API puede manejar errores genéricamente (if (response.error) ...) solo si la forma es predecible — tres formatos distintos según el endpoint obliga a manejar cada caso por separado.

Código de error propio, no solo status HTTP

Sección titulada «Código de error propio, no solo status HTTP»

El status HTTP (400, 404, 409) dice la categoría general; un code propio permite que el cliente reaccione distinto ante errores específicos sin parsear el mensaje de texto (que puede cambiar, o estar en otro idioma):

class AppError extends Error {
constructor(
public status: number,
public code: string,
message: string
) {
super(message)
}
}
throw new AppError(409, "EMAIL_YA_REGISTRADO", "Ese email ya está en uso")
throw new AppError(404, "USUARIO_NO_ENCONTRADO", "Usuario no encontrado")
throw new AppError(400, "VALIDATION_ERROR", "Datos inválidos")
// En el frontend, reaccionar al código específico, no al texto
if (error.code === "EMAIL_YA_REGISTRADO") {
mostrarLinkDeLogin()
}

Para errores que vienen de Zod o express-validator, el cliente generalmente necesita saber qué campo falló, no solo que “algo” falló:

app.use((err, req, res, next) => {
if (err instanceof ZodError) {
return res.status(400).json({
error: {
code: "VALIDATION_ERROR",
message: "Datos inválidos",
fields: err.flatten().fieldErrors // { email: ['Email no válido'], age: [...] }
}
})
}
next(err)
})
export const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
if (err instanceof AppError) {
// Errores "esperados": el mensaje es seguro de mostrar
return res
.status(err.status)
.json({ error: { code: err.code, message: err.message } })
}
// Errores inesperados (bug, falla de DB): loguear completo, NO exponer detalles
console.error(err)
res.status(500).json({
error: { code: "INTERNAL_ERROR", message: "Algo salió mal" }
})
}

Un error de base de datos, un stack trace o el mensaje real de una excepción interna puede filtrar detalles de implementación: nombres de tablas, versiones de librerías o rutas del sistema. Solo los errores operativos creados explícitamente por la aplicación deben exponer un mensaje controlado.

ElementoPara qué
Formato único ({ error: { code, message } })El cliente maneja errores genéricamente
code propioReaccionar a un error específico sin parsear texto
fields en errores de validaciónEl cliente sabe qué campo mostrar en rojo
Errores esperados vs inesperadosSolo los esperados exponen su mensaje real
  • Mensajes en el idioma de la API (o traducidos del lado del cliente usando code) — mezclar textos hardcodeados en un idioma con codes en inglés es común y está bien, siempre que el cliente pueda decidir qué mostrar.
  • Este formato es una convención propia. Para interoperabilidad existe application/problem+json, definido en RFC 9457, que reemplaza RFC 7807. Elige un contrato y aplícalo también a errores del parser, autenticación y límites de tráfico.