Las respuestas de error de tu API son parte de su contrato. Los clientes las analizan, la lógica de reintento se basa en ellas, y los ingenieros de soporte las buscan a las 2 a.m. Sin embargo, la mayoría de los equipos diseñan la ruta feliz en detalle y dejan que los errores surjan de lo que el framework hace por defecto. Así es como terminas con tres formas de error diferentes en una misma API, una respuesta 200 que envuelve "success": false, y un rastreo de pila que filtra el esquema de tu base de datos a la internet pública.
Esta guía cubre las mejores prácticas de manejo de errores de API para servicios REST de principio a fin: elegir el código de estado correcto, estandarizar un solo cuerpo de error con RFC 9457 Problem Details, separar códigos legibles por máquina de mensajes humanos, marcar errores como reintentables, y mantener los secretos fuera de las respuestas. Se basa en nuestro desglose de qué códigos de estado HTTP deben usar las API REST y añade las decisiones a nivel de contrato que esa guía deja abiertas. También verás cómo probar cada ruta de falla en Apidog, porque un contrato de error que nunca pruebas es un contrato que no tienes.
Comienza con el código de estado, no con el cuerpo
HTTP ya te proporciona una primera capa de semántica de errores de forma gratuita. RFC 9110 define las familias de códigos de estado: 4xx significa que el cliente hizo algo mal y repetir la misma solicitud volverá a fallar; 5xx significa que el servidor falló y la solicitud del cliente pudo haber sido correcta. Aclara esta división antes de escribir una sola línea de cuerpo de error, porque los clientes genéricos, los proxies, las cachés y las bibliotecas de reintento se ramifican en ella sin siquiera leer tu JSON.
Los errores más comunes se agrupan en torno a un puñado de pares de aspecto similar. Mantén abierta la referencia de códigos de estado HTTP de MDN mientras diseñas, y usa esta tabla de decisiones para los códigos que confunden a los equipos.
| Situación | Usar | No usar | Por qué |
|---|---|---|---|
| Solicitud mal formada: JSON roto, tipo de contenido incorrecto, campo requerido faltante | 400 Bad Request | 422 | El servidor no puede analizar o entender la solicitud en absoluto |
| Solicitud bien formada que viola reglas semánticas: cantidad negativa, moneda no soportada | 422 Unprocessable Content | 400 | La sintaxis es correcta; los valores no lo son |
| Sin credenciales, o token caducado/inválido | 401 Unauthorized | 403 | El cliente no ha probado quién es. Enviar WWW-Authenticate |
| Credenciales válidas, permisos insuficientes | 403 Forbidden | 401 | La identidad es conocida; el acceso es denegado. Volver a autenticar no ayudará |
| El recurso nunca existió, o no confirmarás que existe | 404 Not Found | 410 | Valor por defecto seguro; también oculta recursos de sondeos no autorizados |
| El recurso existió y fue eliminado deliberada y permanentemente | 410 Gone | 404 | Les dice a los clientes y rastreadores que eliminen sus referencias |
| Conflicto de estado: clave duplicada, versión obsoleta, colisión de edición | 409 Conflict | 400 | La solicitud es válida pero choca con el estado actual del recurso |
| El cliente excedió un límite de tasa | 429 Too Many Requests | 503 | Siempre incluir Retry-After para que los clientes retrocedan correctamente |
| Excepción no manejada en tu código | 500 Internal Server Error | 502 | Tu servidor se rompió |
| El servicio ascendente devolvió basura a tu puerta de enlace | 502 Bad Gateway | 500 | El fallo está corriente abajo del borde, no en él |
| El servidor está sobrecargado o en mantenimiento | 503 Service Unavailable | 500 | Temporal por definición; añade Retry-After cuando puedas |
| El servicio ascendente agotó el tiempo de espera | 504 Gateway Timeout | 500 | Distingue "dependencia lenta" de "código roto" |
Dos de ellos merecen un énfasis especial. Primero, 401 vs 403 es un límite de seguridad, no una elección de estilo: devolver 403 a un llamador no autenticado filtra el hecho de que el recurso existe. Segundo, 429 sin Retry-After entrena a los clientes para que te ataquen en bucles cerrados. Si limitas la tasa, y deberías hacerlo, combina el estado con una señal concreta de retroceso; nuestra guía sobre limitación de tasa de API cubre las matemáticas de los encabezados y los algoritmos detrás de ella.
Una forma de cuerpo de error: RFC 9457 Problem Details
Una vez que el código de estado es correcto, cada error que tu API devuelva debe compartir un tipo de medio y un esquema. La respuesta estándar es RFC 9457 Problem Details, servida como application/problem+json. Define cinco miembros principales: type (una URI que identifica la categoría del error), title (un breve resumen humano), status (el código HTTP, repetido por conveniencia), detail (lo que salió mal en esta ocurrencia), e instance (una URI para este fallo específico). Cualquier otra cosa va en miembros de extensión que definas tú mismo.
No volveremos a derivar la especificación aquí; nuestro explicador de RFC 9457 detalla cada miembro, las reglas de registro y cómo anula a RFC 7807. Lo que importa para tu contrato es el patrón: envoltorio estándar, extensiones personalizadas. Aquí tienes un fallo de validación en un endpoint de pagos.
POST /v1/payments HTTP/1.1
Content-Type: application/json
{ "amount": -1400, "currency": "USD", "source": "card_8xKt2" }
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-error",
"title": "Request validation failed",
"status": 422,
"detail": "One or more fields failed validation.",
"instance": "/v1/payments/requests/req_9f3c1a7b",
"code": "PAYMENT_VALIDATION_FAILED",
"errors": [
{
"field": "amount",
"code": "AMOUNT_NOT_POSITIVE",
"message": "amount must be a positive integer in minor units"
}
],
"request_id": "req_9f3c1a7b"
}
El array errors[] es un miembro de extensión, y es el que más les gusta a los clientes: permite que un frontend mapee cada fallo al campo de formulario exacto en lugar de mostrar un mensaje vago. Mantén las rutas de campo en un formato estable (JSON Pointer o rutas con puntos, elige una) para que el código del cliente pueda vincularlas programáticamente.
Una regla te ahorra la mayor parte del dolor: devuelve esta forma para cada error, incluidos los que genera tu framework o puerta de enlace. Un cliente que recibe Problem Details de tus manejadores pero HTML de la página 502 de tu equilibrador de carga todavía tiene que escribir dos analizadores.
Códigos legibles por máquina vs. mensajes humanos
Observa que el ejemplo incluye campos code y message. Eso es deliberado. Sirven a diferentes audiencias y nunca deben fusionarse en una sola cadena.
Los códigos legibles por máquina (AMOUNT_NOT_POSITIVE, CURRENCY_UNSUPPORTED, IDEMPOTENCY_KEY_REUSED) son contrato. Los clientes se ramifican en ellos, por lo que deben ser estables, documentados y enumerables. Nunca hagas que los clientes analicen prosa; en el momento en que alguien escribe if (message.includes("positive")), tu edición de copia se convierte en un cambio que rompe la compatibilidad.
Los mensajes humanos son lo contrario: libres de mejorar en cualquier momento, escritos para un desarrollador que lee registros y nunca son críticos para el funcionamiento. Indica qué falló y cómo solucionarlo: "la cantidad debe ser un entero positivo en unidades menores" es mejor que "cantidad inválida". Si localizas, localiza el mensaje y deja el código como está.
Esta división importa aún más ahora que los consumidores de API incluyen agentes autónomos. Los clientes basados en LLM se recuperan mucho mejor de errores estructurados y autoexplicativos; cubrimos ese ángulo en diseño de errores de API para agentes de IA.
Lo que nunca va en una respuesta de error
Las respuestas de error son un canal de reconocimiento favorito para los atacantes, porque las fallas no manejadas tienden a ser muy detalladas. Tu middleware de errores debe garantizar que ninguno de los siguientes elementos llegue jamás a un cliente:
- Rastros de pila, nombres de clases o rutas de archivos
- SQL en bruto, fragmentos de consulta o errores de ORM
- Nombres de host internos, IPs, puertos o nombres de servicios
- Versiones de bibliotecas y cadenas de banner del framework
- Secretos, tokens o cadenas de conexión incrustados en el texto de la excepción
- Si una cuenta de usuario existe (en flujos de inicio de sesión y restablecimiento de contraseña, mantén las fallas simétricas)
El patrón es simple: captura todo en el límite, registra la excepción completa en el lado del servidor con un ID de solicitud y devuelve un cuerpo genérico de Problem Details con ese mismo ID. El cliente recibe "detail": "An internal error occurred", "request_id": "req_51ad0", tus registros obtienen la verdad y el soporte puede unir ambos.
Marcar errores como reintentables o terminales
Cada error que devuelves responde a una pregunta que el cliente está a punto de hacer: ¿debo intentarlo de nuevo? Integra la respuesta en el contrato en lugar de dejar que cada equipo cliente adivine.
Los códigos de estado llevan la semántica predeterminada. 429, 502, 503 y 504 son reintentables con retroceso exponencial y fluctuación. 500 es ambiguo pero generalmente vale un reintento cauteloso. Casi todos los demás códigos 4xx son terminales: reintentar un 401, 403, 404 o 422 con la misma solicitud desperdicia cuota y contamina los registros. Los tiempos de espera merecen su propio cuidado, ya que la solicitud puede haber tenido éxito después de que el cliente se rindiera; ese es el clásico problema de tiempo de espera de solicitud 408, y es por eso que los puntos finales de mutación deben aceptar claves de idempotencia para que un pago reintentado no se pueda cobrar dos veces.
También puedes hacer explícita la reintentabilidad con un miembro de extensión:
{
"type": "https://api.example.com/problems/rate-limited",
"title": "Too many requests",
"status": 429,
"code": "RATE_LIMITED",
"retryable": true,
"retry_after_seconds": 30
}
Una bandera explícita retryable te permite anular los valores predeterminados cuando lo necesitas, como marcar un subcódigo 500 específico como terminal porque al reintentarlo se corrompe el estado. Documenta la bandera una vez y cada SDK de cliente que distribuyas obtendrá un comportamiento uniforme de retroceso.
IDs de correlación y versionado de contratos de error
Dos decisiones menores completan el contrato, y ambas son baratas ahora, costosas después.
Asigna un ID a cada solicitud. Acepta un encabezado X-Request-Id entrante (o genera uno), imprímelo en cada línea de registro y repítelo en cada cuerpo de error como request_id. Cuando un cliente pega un error en un ticket de soporte, ese único campo convierte una hora de búsqueda de registros en una sola consulta. En configuraciones distribuidas, propaga un traceparent de W3C junto con él para que el ID siga la solicitud a través de los servicios.
Versiona tu contrato de errores como la propia API. Añadir un nuevo miembro de extensión o un nuevo código de error es seguro. Renombrar errors[].field, cambiar el significado de un código o pasar de una forma ad-hoc a Problem Details es un cambio disruptivo, y rompe las rutas de código que los equipos menos prueban. La URI type te da un mecanismo limpio: mantén las URI de tipo antiguas estables para siempre, introduce nuevas para semánticas nuevas, y declara en tu documentación que los miembros de extensión desconocidos y los códigos desconocidos deben ser ignorados, no tratados como fallos. Esa cláusula de compatibilidad hacia adelante es lo que te permite evolucionar sin una v2.
Prueba cada ruta de error en Apidog
Aquí está la incómoda verdad: los contratos de error se pudren porque nada los ejercita. La ruta feliz se ejecuta en cada demo; la rama 422 se ejecuta cuando un cliente la activa. La solución es hacer que los casos de fallo sean ciudadanos de primera clase en tu conjunto de pruebas, y aquí es donde Apidog se gana su lugar en el flujo de trabajo.
Dos características se mapean directamente a este problema.
Escenarios de prueba para el lado del servidor. Para cada endpoint, construye un escenario por caso de fallo: autenticación faltante espera 401, rol insuficiente espera 403, cantidad negativa espera 422 con errors[0].code igual a AMOUNT_NOT_POSITIVE, tráfico en ráfaga espera 429 con un encabezado Retry-After. Las aserciones visuales de Apidog verifican el estado, los encabezados y los campos del cuerpo sin scripting, y puedes validar toda la carga útil contra tu esquema JSON de Problem Details para que cualquier desviación en la forma del error falle en CI, no en producción. Nuestra guía de aserciones de API muestra los patrones de aserción en detalle.
Servidores simulados para el lado del cliente. Tus equipos de frontend y SDK necesitan construir contra respuestas 4xx y 5xx antes de que el backend pueda producirlas bajo demanda. Los servidores simulados de Apidog devuelven los cuerpos exactos de Problem Details de tu especificación de API, para que puedas simular un 503 con Retry-After: 120, un 409 por envío doble, o una carga útil de validación completa de errors[], luego observa cómo el cliente renderiza y reintenta. Sin un stub de Express hecho a mano, sin comentar código de backend para forzar un fallo.
Diseña el contrato de errores, codifícalo como escenarios y simulaciones, e integra ambos en CI. Descarga Apidog y pruébalo gratis; importar una especificación OpenAPI existente te permite obtener respuestas de error simulables en pocos minutos.
Preguntas Frecuentes
¿Debo usar 400 o 422 para errores de validación?
Usa 400 cuando la solicitud está mal formada y el servidor no puede entenderla: JSON inválido, tipo de contenido incorrecto, un campo requerido faltante. Usa 422 cuando la solicitud se analiza correctamente pero los valores rompen tus reglas de dominio, como una cantidad de pago negativa o una moneda no compatible. El beneficio práctico es diagnóstico: un 422 le dice al cliente "arregla tus datos", mientras que un 400 dice "arregla el formato de tu solicitud". Cualquiera que sea la división que elijas, aplícala consistentemente en cada endpoint.
¿Qué es application/problem+json?
Es el tipo de medio definido por RFC 9457 para Problem Details, el formato estándar de errores JSON para API HTTP. Una respuesta con este tipo de contenido incluye miembros type, title, status, detail e instance, además de cualquier extensión que definas, como un array errors[] para fallos de validación a nivel de campo. Usar el tipo de medio registrado permite que los clientes genéricos y el middleware reconozcan tus errores sin una configuración personalizada. Nuestro explicador de RFC 9457 cubre la especificación completa.
¿Qué errores HTTP deben reintentar automáticamente los clientes?
Reintenta 429, 502, 503 y 504 con retroceso exponencial más jitter, respetando Retry-After cuando esté presente. Trata el 500 como digno de un reintento cuidadoso. No reintentes otras respuestas 4xx; la solicitud fallará de la misma manera cada vez. Para endpoints que modifican datos, combina los reintentos con claves de idempotencia para que una solicitud repetida no pueda cobrar dos veces o crear algo dos veces.
¿Cómo pruebo las respuestas de error de la API sin romper mi backend?
Simúlalos. Apunta tu cliente a un servidor simulado de Apidog que devuelva los cuerpos exactos 4xx y 5xx de tu especificación, luego verifica la renderización y el comportamiento de reintento contra cada uno. En el lado del servidor, escribe escenarios de prueba que envíen cargas útiles inválidas, autenticación faltante y tráfico en ráfaga, luego aserta sobre códigos de estado, encabezados y el esquema del cuerpo del error. Ambas partes se ejecutan en CI, por lo que el contrato de errores se mantiene honesto sin que nadie fuerce manualmente los fallos.
