Publica un código de error estable y legible por máquina
Tus mensajes de error son una API secreta. Los consumidores les aplican regex, y luego no puedes cambiarlos.
Escribiste "User not found" para ayudar a un humano depurando en un log. Pero algún cliente no tenía otra forma de ramificar sobre tus errores, así que escribió if "not found" in err.message. Ahora esa frase es estructural. Corrige un typo, agrega contexto, tradúcelo, y su código se rompe en producción. Firmaste un contrato que nunca quisiste firmar.
El texto del error es un contrato accidental
Esto es la ley de Hyrum otra vez: cualquier cosa que los consumidores puedan observar, algún consumidor dependerá de ella. Los strings de error son la versión más seductora porque parecen un detalle interno. No lo son. Si un mensaje es la única señal distinguible por máquina que emites, los integradores lo parsearán, y la prosa es la peor interfaz posible: inestable, localizada y sin tipos.
Publica un código sobre el que ramificar
Separa las dos audiencias. Dale a las máquinas un código estable y enumerado. Dale a los humanos un mensaje que quede libre de cambiar.
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "You've sent 1001 requests this minute; the limit is 1000.",
"retryable": true,
"retry_after_ms": 4200
}
}
El code es el contrato: un vocabulario cerrado de símbolos estables sobre los que ramificar. El message es documentación para humanos: reescríbelo, agrega detalle, localízalo, y ninguna integración se rompe. Nunca hagas que los consumidores parseen texto para tomar una decisión de flujo.
Define los códigos como un enum en un solo lugar para que sean descubribles y difíciles de tipear mal:
class ErrorCode(str, Enum):
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
INVALID_ARGUMENT = "INVALID_ARGUMENT"
NOT_FOUND = "NOT_FOUND"
INTERNAL = "INTERNAL"
Marca qué es seguro reintentar
Los consumidores también necesitan saber si reintentar es seguro, y la prosa no se los dirá. Codifícalo. La convención HTTP es un buen valor por defecto: 4xx significa que la petición misma está mal — no reintentes, arreglarla requiere un cambio. 5xx significa una falla del lado del servidor — un reintento puede tener éxito. Hazlo explícito con un booleano retryable y, cuando aplique, una pista retry_after para que los clientes esperen en vez de martillarte.
| Clase | Significado | ¿Reintentar? |
|---|---|---|
| 4xx (excepto 408/429) | La petición del cliente es inválida | No — fallará de nuevo |
| 429 | Límite de tasa alcanzado | Sí, tras retry_after |
| 5xx | Falla del lado del servidor | Sí, con backoff |
Existen estándares para que no lo inventes: RFC 9457 "Problem Details for HTTP APIs" define un type (URI estable), title y detail, que mapean limpiamente a código estable más mensaje libre.
Movidas avanzadas
- Usa namespaces en tus códigos.
BILLING.CARD_DECLINEDes mejor que unCARD_DECLINEDplano: evita que los vocabularios colisionen a medida que la API crece. - Nunca reutilices un código retirado con un nuevo significado. Los códigos son para siempre, como los valores de un enum en el cable. Agrega nuevos; deprecia los viejos.
- Incluye un ID de traza/correlación en el payload para que soporte encuentre la falla exacta sin que los consumidores peguen texto del mensaje.
- Documenta la lista de códigos como parte de la referencia de la API, con la semántica de reintento por código. Eso es el contrato; los mensajes no.
Recursos
- RFC 9457 — Problem Details for HTTP APIs
- Google API Design Guide — Errors
- Códigos de estado de gRPC y su semántica de reintento
- La ley de Hyrum
¿Desarrollas sistemas de IA o en la nube? Yeda AI audita y refuerza pipelines de LLM y APIs en producción. Hablemos · Lee el blog