2.20

Ver en inglés

2.20 Manejo de errores y patrones de resiliencia

Visión general y motivación

Todo programa que se escriba fallará. Un disco se llena, una red se interrumpe, un servicio agota el tiempo de espera, un invocador le pasa datos inválidos, una dependencia devuelve algo que los documentos jamás mencionaron. La pregunta no es si el fallo ocurrirá, sino si su código lo recibirá con un plan o con una sorpresa. El manejo de errores es el oficio de decidir, línea a línea y función por función, qué hace su código cuando el mundo no coopera. Es la parte menos llamativa de la construcción y, sin embargo, la que más que cualquier función determina si la gente confía en su sistema.

Este capítulo trata sobre la resiliencia a nivel de código y de componente: las decisiones que se toman dentro de una función, un módulo o una API. Complementa el capítulo 3.5, que aborda la resiliencia a nivel de sistema (equilibrado de carga, replicación, conmutación por error entre servicios). El capítulo 3.5 mantiene de pie toda la plataforma cuando una región se cae; este capítulo impide que una sola solicitud corrompa sus datos o desaparezca sin dejar rastro. Ambos se refuerzan. Un disyuntor en su arquitectura no sirve de nada si el código que hay detrás traga excepciones, y una función defensiva no le salvará si el sistema que la rodea carece de redundancia. Este capítulo además se apoya en el 2.9 (construcción de software), donde el manejo de errores era una disciplina entre muchas; aquí, se convierte en el tema central.

En equipos grandes, la consistencia es el premio. Cuando cientos de ingenieros manejan los errores a su manera, cada servicio se convierte en un acertijo y cada incidente en una excavación. En entornos empresariales, esa inconsistencia encarece cada auditoría y cada integración. En gobierno y otros sistemas de alta responsabilidad, las exigencias son más agudas: la corrección, el fallo seguro y un rastro de auditoría claro no son funcionalidades que se añadan después, sino propiedades que el sistema debe tener desde el primer compromiso de código. Un sistema de prestaciones que calcula de forma errónea en silencio, o un sistema de registros que pierde un fallo sin registrarlo, no es simplemente defectuoso. Es digno de desconfianza de una manera que erosiona la institución que lo sostiene.

Principios clave

  • Distinga entre fallas, errores y averías, y maneje cada uno en su capa correspondiente.
  • Elija entre fallo rápido o fallo seguro de forma deliberada, según el contexto, nunca por accidente.
  • Haga explícito y honesto el contrato de manejo de errores de cada función y API.
  • Valide en las fronteras; confíe en el interior; defienda sin paranoias.
  • Nunca trague un error en silencio; exponga, envolva o maneje con propósito.
  • Haga que los reintentos sean seguros mediante idempotencia, tiempos de espera, retraso progresivo y variación aleatoria.
  • Dote a la ruta de error de la misma atención de diseño que la ruta exitosa.

Recomendaciones

Distinga entre fallas, errores y averías

Un vocabulario impreciso genera un manejo impreciso, así que empiece por términos claros. Una falla es un defecto del sistema: un bug, una configuración errónea, una dependencia caída. Un error es el estado interno incorrecto que esa falla produce: un valor nulo donde debería haber uno, un saldo que ya no cuadra. Una avería es lo que observa el exterior: la solicitud devuelve una respuesta incorrecta o no devuelve ninguna. Una sola falla puede generar muchos errores, y muchos errores pueden interceptarse antes de que se conviertan en una avería visible. Todo el punto del manejo de errores es interrumpir esa cadena, captar el error antes de que se transforme en una avería que el usuario o el auditor experimente.

Este vocabulario también le indica dónde actuar. Las fallas se abordan en la revisión, la prueba y la configuración. Los errores se abordan en tiempo de ejecución con los patrones de este capítulo. Las averías se abordan con la observabilidad (capítulo 9.2) y con la resiliencia a nivel de sistema del capítulo 3.5. Cuando su equipo comparte estas palabras, las revisiones de incidentes se afilan: puede decir con precisión dónde debía interrumpirse la cadena y no lo hizo, en lugar de discutir sobre qué fue «el bug».

Elija fallo rápido o fallo seguro según el contexto

El fallo rápido consiste en detenerse en el momento en que algo sale mal, negarse a continuar sobre un estado corrupto para que el problema aparezca con estruendo y cerca de su origen. El fallo seguro consiste en degradarse hacia un estado conocido e inofensivo y seguir sirviendo lo que con seguridad pueda servirse. Ninguno es universalmente correcto, y la destreza está en elegir según el contexto. Durante el desarrollo y en las fronteras internas, el fallo rápido es su aliado: un programa que se detiene ante una invariante violada le entrega una traza de pila corta en lugar de un misterio larguísimo. En producción, en los bordes de un sistema orientado al usuario, el fallo seguro suele imponerse: un panel de recomendaciones que no muestra nada es mejor que una página de pago que no carga.

Tome esta decisión de forma deliberada para cada frontera y documéntela. Un componente de control de vuelo o un dispositivo médico falla de forma segura hacia un estado definido, porque continuar sobre datos corruptos podría herir a alguien. Una partida contable falla rápido, porque registrar una entrada errónea es peor que no registrar ninguna. El emparejamiento equivocado es peligroso en ambas direcciones: el fallo seguro donde se necesitaba fallo rápido oculta la corrupción, y el fallo rápido donde se necesitaba fallo seguro convierte un tropiezo cosmético en una interrupción.

Elija su mecanismo de señalización de errores y úselo de forma consistente

Los lenguajes ofrecen dos formas amplias de indicar que algo ha salido mal. El manejo de excepciones arroja un objeto a lo largo de la pila de llamadas hasta que algún manejador lo capta, separando la ruta de error de la lógica principal. La alternativa son los valores de error explícitos: la función devuelve tanto el resultado como el error, y el invocador debe inspeccionar ambos. Muchos lenguajes modernos formalizan esta última con un tipo Result, a menudo llamado Result o Either, que obliga al invocador a desenpaquetar o el éxito o el fracaso antes de usar el valor. Cada enfoque tiene un costo. Las excepciones mantienen la ruta exitosa limpia, pero pueden ocultar el flujo de control y tientan a los desarrolladores a crear bloques de captura genéricos que borran información. Los resultados explícitos hacen que cada fracaso sea visible en la firma del tipo, pero añaden ceremonias y pueden ignorarse si el lenguaje no fuerza la verificación.

La respuesta correcta tiene que ver menos con el mecanismo y más con la consistencia y la honestidad. Elija el idiomático que favorezcan su lenguaje y su ecosistema, y aplíquelo de forma uniforme en todos sus servicios para que quien lea el código sepa siempre cómo viaja el fracaso. Reserve las excepciones para condiciones genuinamente excepcionales, no para flujos de control ordinarios como «usuario no encontrado», que conviene modelar como un resultado normal. Sea lo que sea lo que elija, nunca permita que un fracaso se vuelva invisible: un valor de error no verificado es tan peligroso como un bloque de captura vacío. En un código extenso, una convención documentada junto a un corrector que señale los errores ignorados supera cualquier preferencia individual.

Haga explícito el contrato de manejo de errores

Cada función y cada API tiene un contrato de manejo de errores, se haya escrito o no. Este contrato responde: ¿qué puede fallar aquí, cómo se enterará usted, y qué se garantiza sobre el estado cuando falla? Hágalo explícito. Documente qué errores puede devolver o lanzar una función, distinga los errores recuperables (el invocador puede reintentar o recurrir a un plan alternativo de forma razonable) de los irreverables (el invocador no puede resolverlos y debe propagarlos o abortar), y declare si la función deja el estado inalterado ante el fracaso. Esta última propiedad, a veces llamada garantía fuerte de excepción, significa que una llamada fallida es como si nunca hubiera ocurrido, que es exactamente lo que permite a un invocador reintentar con seguridad.

Para una API pública o interequipo, este contrato es parte de la interfaz, tan real como los tipos de parámetro. Diseñe una pequeña taxonomía de errores estable: un conjunto acotado de categorías como error de validación, no encontrado, conflicto, no autorizado, dependencia no disponible y error interno. Así, los invocadores pueden ramificar según la categoría sin analizar cadenas de texto. Una taxonomía clara hace que el manejo de errores sea componible a través de muchos servicios y hace que las averías sean auditable, porque cada fallo se remite a una categoría conocida y con nombre.

Valide en las fronteras y defienda sin paranoias

Trate los datos que cruzan una frontera de confianza (una solicitud de red, un archivo, una entrada del usuario, un mensaje de otro servicio) como hostiles hasta que se validen, y valide en la frontera, una vez, a fondo. Es programación defensiva aplicada con criterio. Dentro de un módulo cuyos inputs ya se validaron, los controles redundantes en cada línea ocultan la lógica y suprimen precisamente los fallos que uno querría ver. La disciplina es: defender con firmeza en los bordes, confiar en el interior. Valide la estructura, los rangos y las invariantes allí donde entran los datos, conviértalos en tipos que hagan inrepresentables los estados ilegales, y permita que el código interno asuma que trabaja con datos limpios.

La paranoia tiene un costo real. Un código ahogado en comprobaciones de nulo y ramificaciones defensivas es más difícil de leer y, peor aún, a menudo convierte un fallo claro en un gesto silencioso, devolviendo un valor predeterminado donde debería habersonado una alarma. Una defensividad que oculta bugs no es seguridad; es aplazamiento.

Haga que los reintentos sean seguros, acotados y corteses

Muchas fallas son transitorias: un cortocircuito de red momentáneo, un servicio que se reinicia, una breve contención de bloqueo. En cualquier sistema distribuido (capítulo 3.3), estos fallos parciales son el caso normal más que la excepción. El reintento es la respuesta natural, pero un bucle de reintentos ingenuo es un arma cargada. Primero, haga que la operación que reintenta sea idempotente, es decir, que ejecutarla dos veces tenga el mismo efecto que ejecutarla una. Sin idempotencia, un reintento tras un tiempo de espera agotado puede cobrar una tarjeta dos veces o crear dos registros, porque no puede saber si el primer intento falló o si simplemente se perdió su confirmación. Use claves de idempotencia para las escrituras, de modo que el receptor pueda reconocer y deduplicar un repetición.

Segundo, ponga un tiempo de espera agotado en cada llamada remota, para que una dependencia colgada no lo paralice a usted. Tercero, espacé los reintentos con retraso exponencial, duplicando la espera tras cada intento, y añada variación aleatoria (un pequeño retardo aleatorio) para que mil clientes que se recuperan a la vez no se sincronicen en una estampida que vuelva a tumbar el servicio que intenta recuperarse. Cuarto, limite el número de reintentos y el tiempo total, y después abandone con elegancia. Los reintentos sin límites, sin retraso progresivo, sin variación aleatoria y sin idempotencia son una de las formas más comunes en que un tropiezo menor se convierte en una interrupción autoinfligida.

Añada disyuntores, compartimentos estancos y degradación elegante en el código

Cuando una dependencia está genuinamente caída, reintentarla solo desperdicia esfuerzo y ahonda el agujero. Un disyuntor vigila la tasa de fallas de las llamadas a una dependencia y, una vez que las fallas superan un umbral, «se abre» para fallar de inmediato durante un período de refrigeración en lugar de esperar a llamadas condenadas. Tras la refrigeración, deja pasar una llamada de prueba y se vuelve a cerrar si la dependencia se ha recuperado. Esto protege tanto a sus invocadores (fallas rápidas y predecibles en lugar de tiempos de espera apilados) como a la dependencia en apuros (aire para recuperarse). El patrón de compartimento estanco, tomado de los compartimentos de un barco, aísla los recursos para que una dependencia saturada no consuma todas las hebras ni todas las conexiones y hunda el proceso completo; se le asigna a cada dependencia su propio grupo acotado.

Estos patrones se combinan con la degradación elegante a nivel de código: cuando una dependencia no esencial no está disponible, devuelva un resultado reducido pero útil en lugar de un error. Muestre datos cacheados con una nota de antigüedad, oculte el panel de personalización, encolere la escritura para después. Esta es la contraparte local de la resiliencia a nivel de sistema del capítulo 3.5: la arquitectura provee redundancia entre máquinas, y su código provee un comportamiento sensato cuando falta una pieza.

Envuelva los errores con contexto y nunca los trague

Un error que dice «conexión rechazada» diez capas por encima de donde ocurrió es casi inútil. A medida que un error se propaga, envuélvalo con contexto: qué intentaba hacer, sobre qué entidad o solicitud, qué dependencia, preservando la causa original para que el origen no se pierda. Buenos lenguajes y bibliotecas lo apoyan directamente mediante la cadena de errores. El objetivo es que una sola línea de registro le diga al ingeniero de guardia qué falló, durante qué operación y para qué entrada. Esto es la materia prima para la observabilidad del capítulo 9.2 y la depuración del capítulo 2.15.

El pecado cardinal es tragar un error: un bloque de captura vacío, un valor de retorno ignorado, una captura que registra en nivel de depuración y continúa como si no hubiera pasado nada. Un error tragado no desaparece; reaparece más tarde como datos corruptos o un defecto inexplicable, ya desligado de su causa. Cada error debe cumplir una de tres funciones: manejarlo (recuperar o degradar), envolverlo y propagarlo, o, en la cima de la pila, registrarlo con todo el contexto y fallar. Si captura un error y no hace ninguna de estas cosas, ha elegido ocultar un incidente futuro a su futuro yo.

Compromisos: ventajas e inconvenientes

EnfoqueVentajasInconvenientes
ExcepcionesRuta exitosa limpia; difícil de ignorar si no se capturanFlujo de control oculto; tientan a la captura genérica
Valores de error explícitos / tipos ResultFallo visible en la firma; fuerza el manejoMás ceremonias; pueden ignorarse sin coerción
Fallo rápidoExpone los bugs con estruendo, cerca del origenExperiencia deficiente si se usa en el borde
Fallo seguroSigue sirviendo; protege a usuarios y datosPuede enmascarar corrupción si se usa donde hacía falta fallo rápido
Reintentos con retraso progresivoSoporta fallas transitorias automáticamenteAmplifica la carga y duplica escrituras sin idempotencia
DisyuntorFallas rápidas; permite que las dependencias se recuperenAñade estado y ajuste fino; puede enmascarar un problema persistente
Validación defensiva en fronterasAtrapa datos defectuosos pronto, una vez, con estruendoEn exceso, recarga la lógica y oculta fallos reales

La tensión central es entre visibilidad y ruido. Manejar los errores en exceso de forma silenciosa oculta problemas hasta que son caros; manejarlos con estruendo en todas partes ahoga la señal en ceremonias y enmascara los fallos que importan. Resuélvela por ubicación e intención. Sea estridente y estricto en las fronteras, donde entran los datos defectuosos y las fallas de dependencias. Sea sereno y confiado en el interior, donde los inputs ya están limpios. Decida entre fallo rápido y fallo seguro por frontera y documéntelo. El objetivo es un código donde cada fallo tiene exactamente un propietario claro y un destino claro, y nada cae en silencio entre las grietas.

Preguntas para debatir con su equipo

  1. ¿Tienen una taxonomía de errores y una convención de manejo compartida en todos sus servicios, o cada equipo improvisa? En un equipo grande, esta es la diferencia entre fallos que se componen y fallos que confunden. Cuando un servicio devuelve HTTP 500 por un problema de validación, otro lanza una excepción tipada y un tercero devuelve un nulo, cada integración se convierte en una negociación y cada incidente en un ejercicio de traducción. Traiga ejemplos del mismo fallo lógico, digamos «registro no encontrado», tal como aparece en tres de sus servicios, y observe lo distintos que son. La respuesta debe ser un estándar escrito: un conjunto acotado de categorías de error, una forma coherente de señalearlos y un corrector o lista de revisión que lo haga cumplir. La consistencia aquí rinde en cada integración futura, cada auditoría y cada turno de guardia.

  2. ¿Para cada frontera crítica, han elegido fallo rápido o fallo seguro a propósito, y el código coincide con esa elección? La mayoría de los equipos nunca ha tomado esta decisión de forma explícita, lo que significa que la tomó por ellos quien escribió el código primero, y de forma inconsistente. Las consideraciones enfrentadas son reales: el fallo seguro mantiene a los usuarios servidos pero puede dejar que la corrupción se extienda, mientras que el fallo rápido protege los datos pero puede convertir la caída menor de una dependencia en una avería visible. Traiga su historial de incidentes y pregunte, para los peores, si el código falló como habrían elegido ellos de antemano. La evidencia que buscan es un mapa de sus fronteras con una etiqueta deliberada en cada una, sobre todo donde interviene dinero, seguridad o registros de ciudadanos. Donde la etiqueta y el código no coinciden, ha encontrado su próxima corrección.

  3. ¿Cuándo ejercitaron a propósito una ruta de error, y se comportó como se diseñó? La ruta de error suele ser el código menos probado que poseen, y es donde se gana o se pierde la confianza. Y la afirmación de «fallamos de forma segura» no se puede sostener si nunca la han visto en acción. Un bucle de reintentos sin idempotencia, un disyuntor cuyo umbral es erróneo, una excepción tragada en una rama poco frecuentada: todo esto se esconde hasta que un incidente real los descubre. Traiga los resultados de inyectar fallo deliberadamente (una dependencia eliminada, un tiempo de espera inducido, una carga malformada) en un entorno realista. La acción que sigue es hacer de la inyección de fallos algo rutinario, para que la recuperación, la degradación y el comportamiento de fallo seguro se verifiquen de forma continua y no se dejen a la esperanza. Toda ruta de error que nunca se ha activado es una promesa que no se ha probado.

  4. ¿Cuáles de sus operaciones de escritura son idempotentes y en cuáles un reintento tras una confirmación perdida duplicaría un efecto en el mundo real, como un pago o un registro? El reintento es el reflejo de resiliencia más común y, ejecutado con descuido, la forma más frecuente en que un cortocircuito transitorio se convierte en dinero duplicado o datos duplicados. En un equipo grande, la lógica de reintento suele vivir a la vez en clientes compartidos, intermediarios y servicios individuales, de modo que una sola escritura puede reintentarse en varias capas sin que nadie sea responsable del comportamiento global. La tensión enfrentada es que las claves de idempotencia, la deduplicación y el almacenamiento de resultados de solicitudes añaden almacenamiento y código, y equipos bajo presión de entrega las saltan por escrituras que dan por seguras erróneamente. Traiga un inventario de sus escrituras visibles externamente, cada una marcada con si lleva clave de idempotencia y cómo el receptor reconoce y deduplica una repetición. En entornos empresariales y gubernamentales, señale primero los que mueven dinero o alteran un registro de ciudadano, porque un pago duplicado o una prestación duplicada es un hallazgo de auditoría y a veces una exposición legal, no solo un defecto.

  5. ¿Sus tiempos de espera, disyuntores y compartimentos estancos provienen de una biblioteca compartida y probada, o cada equipo los implementa por su cuenta? Estos patrones son fáciles de describir y fáciles de hacer mal: un tiempo de espera ausente, un umbral de disyuntor que nunca se dispara, un grupo de conexiones dimensionado de modo que una dependencia lenta asfixia el proceso completo. Cuando cada equipo los reimplementa, se acumulan copias ligeramente defectuosas y no hay un único lugar donde corregir una falla una vez que se encuentra. La consideración enfrentada es que una biblioteca compartida impone una interfaz común y un ritmo de actualización, y equipos con entornos o necesidades de latencia atípicas pueden resistirse o rodearla. Traiga un sondeo de cuántas implementaciones distintas de reintento y disyuntor corren en producción, y qué servicios carecen todavía de tiempo de espera en sus llamadas salientes. Para una gran empresa o agencia, una biblioteca acreditada también ofrece a los revisores de seguridad y a los auditores un solo componente que certificar en lugar de decenas, lo que reduce el costo de cada revisión.

  6. ¿Si anoche hubiera un incidente, cualquier ingeniero de guardia podría rastrearlo desde una sola línea de registro, y un auditor podría ver después cada fallo que el sistema registró? Un error envuelto, categorizado y bien registrado es la diferencia entre un diagnóstico de diez minutos y una excavación de medianoche, y un error tragado es un incidente futuro que se ha ocultado a uno mismo. En un equipo grande, los fallos cruzan muchos saltos entre servicios, de modo que el valor viene de un contexto coherente e identificadores de correlación que sobreviven esos saltos, no de la diligencia de un solo equipo. La tensión enfrentada es el costo y el ruido: registrar todo ahoga la señal y obliga a pagarlo en almacenamiento; registrar muy poco impide reconstruir lo ocurrido. Traiga una falla reciente real y recorra su rastro de extremo a extremo, anotando cada salto donde el contexto se perdió o un error fue capturado y descartado. En sistemas regulados y de gobierno, trate esto como propiedad de cumplimiento, porque un fallo no auditable, o una decisión que no se puede explicar años después, es una exposición legal y no solo una brecha operativa.

Perspectiva por sector

Startup. Con un puñado de ingenieros y ninguna holgura que regalar, gaste su presupuesto de manejo de errores donde una falla le cuesta un cliente o sus datos: ponga un tiempo de espera en cada llamada saliente, haga que sus escrituras que mueven dinero sean idempotentes y añada una regla de corrección contra errores ignorados. Omita el marco elaborado; un tipo Result para las funciones núcleo y una degradación elegante en dependencias no críticas le brindan la mayor parte de la seguridad por unos días de trabajo. Fuelle rápido en desarrollo para que los bugs aparezcan con estruendo, y resista la tentación de implementar un disyuntor a mano antes de tener de hecho una dependencia que lo justifique.

Pequeña empresa. Sin un especialista en resiliencia en plantilla y con un presupuesto ajustado, apoye en lo que su lenguaje, su marco y su proveedor en la nube ya le ofrecen en lugar de construir patrones desde cero: colas gestionadas, reintentos del lado del proveedor y tiempos de espera de bibliotecas cubren más de lo que la mayoría de equipos espera. Formule la decisión como comprar o construir, y compre donde una dependencia madura le ofrezca reintentos, retraso progresivo e idempotencia. Dirija su atención escasa a la una o dos fronteras donde una transacción errónea o perdida le haría daño real, y asegúrese de que esas fronteras fallen de forma segura y dejen un rastro.

Gran empresa. A través de muchos equipos, el premio es la consistencia: una taxonomía de errores compartida, una biblioteca común para tiempos de espera, reintentos, disyuntores y compartimentos estancos, y un corrector y una lista de revisión que los hagan cumplir en la línea de integración. Cada error debe alimentarse a una plataforma de observabilidad unificada con identificadores de correlación, para que una avería sea trazable a través de saltos entre servicios, y las decisiones de fallo rápido versus fallo seguro por frontera se estandaricen, para que las auditorías encuentren un patrón documentado y defendible en lugar de una dispersión de hábitos locales. Gobierna la biblioteca compartida como un producto de verdad, porque una falla corregida allí es una falla corregida en todas partes.

Sector público. La corrección, el fallo seguro y un rastro de auditoría durable son obligaciones, no preferencias. Fuelle rápido ante cualquier invariante violada que toque dinero o elegibilidad, valide toda entrada orientada al ciudadano en la frontera, y escriba cada fallo en un registro de auditoría inmutable con suficiente contexto para que una decisión se pueda explicar y revisar años después. La contratación pública y la larga vida de los sistemas exigen que los contratos de error estén documentados para que los funcionarios puedan mantener el código mucho después de que los autores originales hayan partido, y cualquier componente de un proveedor debe exponer su comportamiento ante el fallo en lugar de ocultarlo tras una interfaz opaca.

Ejemplos

Startup. Un equipo de cuatro personas lanza una aplicación que llama a un proveedor de pago externo y a un servicio de correo. Al principio añaden un bucle de reintentos ingenuo y, de inmediato, cargan dos veces a un cliente cuando un tiempo de espera enmascara un cargo exitoso. La lección enseña: añaden claves de idempotencia a cada escritura, ponen un tiempo de espera en cada llamada saliente y cambian a retraso exponencial con variación aleatoria. Adoptan un tipo Result para las funciones de servicio núcleo, de modo que el fracaso aparece en la firma, y una regla de corrección señala cualquier error ignorado. Cuando el envío de un correo falla, el pago se degrada elegantemente encolando el mensaje en lugar de bloquear la venta. La disciplina cuesta unos días y les ahorra una clase de incidentes que les habría costado mucho más en reembolsos y confianza.

Gran empresa. Una compañía logística global opera cientos de servicios y estandariza el manejo de errores en todos ellos. Cada servicio remite las fallas a una taxonomía compartida (validación, no encontrado, conflicto, dependencia no disponible, error interno), para que los invocadores ramifiquen según la categoría en lugar de analizar mensajes. Una biblioteca común ofrece disyuntores, reintentos acotados con retraso progresivo y variación aleatoria, y grupos de conexiones compartimentados, de modo que nadie reimplementa estos patrones a su manera. Cada error se registra con contexto de correlación que alimenta la plataforma de observabilidad del capítulo 9.2, para que un ingeniero de guardia pueda rastrear una avería a través de saltos entre servicios desde una sola línea. Como el estándar es uniforme y se hace cumplir en la línea de integración, los ingenieros se mueven con seguridad por servicios desconocidos y los auditores pueden ver que cada fallo está registrado, categorizado y trazable.

Sector público. Una agencia nacional de prestaciones construye un sistema de elegibilidad y pago donde una respuesta errónea puede negarle el alquiler a alguien o pagar de más desde el erario público. La corrección y el fallo seguro son innegociables, así que el código fuelle rápido ante cualquier invariante financiera violada: un cálculo que no cuadra se niega a registrarse en lugar de registrar una cifra errónea. Toda entrada orientada al ciudadano se valida en la frontera, y los estados ilegales se hacen inrepresentables en los tipos de dominio. Cada fallo se escribe en un registro de auditoría inmutable con contexto completo, cumpliendo el requisito legal de que las decisiones sean explicables y revisables años después. Cuando una dependencia no esencial, como la vista previa de documentos, no está disponible, el sistema se degrada elegantemente para que el trabajador social pueda seguir tramitando la solicitud. Los nuevos funcionarios heredan un código cuyos contratos de error están documentados, para que puedan mantenerlo con seguridad mucho después de que los autores originales hayan partido.

Caso de negocio: motivaciones, retorno y costo total

El retorno de un manejo de errores disciplinado se manifiesta en menos incidentes, incidentes más cortos y más baratos. La mayoría de las interrupciones en producción no son exóticas; se rastrean a una excepción tragada, un tiempo de espera ausente, una tormenta de reintentos o una frontera que confió en datos que debía validar. Cada uno es prevenible con los patrones de este capítulo, y cada incidente evitado no solo ahorra el costo directo del tiempo inactivo, sino los costos compuestos de la respuesta de emergencia, la pérdida de clientes y la investigación. Como un error envuelto y bien registrado puede diagnosticarse en minutos en lugar de horas, el tiempo medio de recuperación cae, y con él la tasa de fallo del cambio, a medida que los ingenieros dejan de temer a la ruta de error.

El costo de adoptar es modesto y en su mayor parte de un solo uso. Se escribe una taxonomía de errores, se provee una biblioteca compartida para reintentos y disyuntores para que los equipos no los reimplementen mal, se añaden reglas de corrección contra errores ignorados, y se construye el hábito de la inyección de fallos. El costo de la negligencia se acumula en silencio: los errores tragados se sedimentan en datos corruptos caros de revertir, y el manejo inconsistente multiplica el costo de cada integración y cada auditoría. En entornos regulados y de gobierno, un fallo no auditable es una exposición de cumplimiento y legal, no solo un problema de ingeniería. Para hacer el caso ante la dirección, vincule la disciplina del manejo de errores con las métricas que ya vigilan: frecuencia de incidentes, tiempo medio de recuperación, tasa de fallo del cambio y hallazgos de auditoría.

Antipatrones y trampas

  • Trago silencioso: bloques de captura vacíos y valores de retorno ignorados que convierten un fallo en un misterio tardío y desligado.
  • Captura genérica: una captura amplia que registra un mensaje genérico y descarta el error original y su contexto.
  • Reintento sin idempotencia: volver a ejecutar escrituras no idempotentes tras un tiempo de espera agotado, cargando dos veces o duplicando registros.
  • Tormentas de reintentos: sin retraso progresivo, sin variación aleatoria y sin límite, de modo que los clientes se sincronizan y asedian la dependencia que intenta recuperarse.
  • Sin tiempos de espera: llamadas remotas sin límite que permiten a una dependencia colgada agotar las hebras y congelar el proceso completo.
  • Excepciones como flujo de control: lanzar y capturar para resultados ordinarios como «no encontrado», ocultando la lógica y ralentizando el código.
  • Paranoia defensiva: comprobaciones en cada línea que sepultan la lógica y convierten fallos reales en valores predeterminados silenciosos.
  • Errores como texto: invocadores que analizan la cadena de mensajes de error porque no hay una taxonomía estable y categorizada sobre la cual ramificar.
  • Fallo seguro donde hacía falta fallo rápido: continuar sobre un estado corrupto en un sistema donde una respuesta errónea es peor que ninguna.

Modelo de madurez

  • Nivel 1, Iniciación: El manejo de errores es ad hoc y reactivo, decidido por cada desarrollador. Los bloques de captura vacíos y los retornos ignorados son frecuentes, los reintentos son ingenuos, los tiempos de espera faltan y los fallos se manifiestan como datos corruptos o defectos sin explicación, sin registro coherente.
  • Nivel 2, Desarrollo: Los equipos adoptan prácticas básicas, pero de forma inconsistente. Los errores se registran con algo de contexto, el trago obvio se desaconseja en la revisión, y existen tiempos de espera y reintentos simples, pero las convenciones varían entre servicios, la idempotencia es irregular y la ruta de error rara vez se prueba.
  • Nivel 3, Estandarización: Una taxonomía de errores y una convención de manejo están documentadas y se hacen cumplir en toda la organización. La validación en fronteras, los reintentos idempotentes con retraso progresivo y variación aleatoria, los disyuntores, los compartimentos estancos y el envoltorio de errores son estándar, provistos por bibliotecas comunes, y cada error alimenta un conducto de observabilidad unificado.
  • Nivel 4, Gestión: El comportamiento de manejo de errores se mide contra líneas base y se controla con datos. Las tasas de reintento, los disparos de disyuntor, los conteos de tiempos de espera agotados, los hallazgos de errores tragados por análisis estático, el tiempo medio de recuperación y la tasa de fallo del cambio se rastrean por servicio; los umbrales de los disyuntores y los tiempos de espera se ajustan a partir de datos observados de latencia y fallos, no a conjeturas; la inyección de fallos se ejecuta con un calendario; y los equipos revisan estas métricas para captar regresiones y sujetar cada elección de fallo rápido o fallo seguro a la evidencia.
  • Nivel 5, Orquestación: La resiliencia se integra en la entrega y la planificación de riesgos, y se mejora de forma continua. La taxonomía, las bibliotecas compartidas y los estándares evolucionan a partir de cada incidente, los experimentos de caos y de inyección de fallos son rutinarios, y la organización adapta los tiempos de espera, los umbrales de disyuntor, las estrategias de degradación y las decisiones de frontera a medida que cambian el tráfico, las dependencias y el panorama de riesgo.

Ideas para la discusión

  1. ¿Dónde en su código un error se traga actualmente, y cómo sabría si se equivoca al pensar que no?
  2. ¿Cuáles de sus operaciones de escritura son idempotentes y cuáles ejecutarían dos veces si un reintento se dispara tras una confirmación perdida?
  3. Debería «usuario no encontrado» ser una excepción, un valor de error o un resultado normal, y ¿su equipo responde de forma coherente?
  4. ¿Cuál es su regla real sobre dónde se valida, y puede señalar una frontera que confía en datos que no debería?
  5. ¿Cómo decide el umbral y la refrigeración de un disyuntor, y cómo sabría que la configuración actual es errónea?
  6. Si un auditor pidiera ver cada fallo que su sistema experimentó el mes pasado, ¿podría producirlo, categorizado y con contexto?

Puntos clave

  • Distinga entre fallas, errores y averías, y interrumpa la cadena antes de que un error interno se convierta en una avería visible.
  • Elija entre fallo rápido y fallo seguro de forma deliberada por frontera, y haga explícito el contrato de manejo de errores de cada función.
  • Valide con firmeza en las fronteras de confianza y confíe en el interior; una defensividad que enmascara fallos es aplazamiento, no seguridad.
  • Haga que los reintentos sean seguros con idempotencia, tiempos de espera, retraso exponencial y variación aleatoria, y añada disyuntores y degradación elegante en el código.
  • Envuelva los errores con contexto, aliménteles a la observabilidad y nunca los trague; cada error debe manejarse, propagarse o registrarse y exponerse.

Referencias y lecturas adicionales

  • Michael T. Nygard, Release It! Design and Deploy Production-Ready Software
  • Andrew Hunt y David Thomas, The Pragmatic Programmer
  • Steve McConnell, Code Complete: A Practical Handbook of Software Construction
  • Betsy Beyer, Chris Jones, Jennifer Petoff y Niall Richard Murphy (eds.), Site Reliability Engineering: How Google Runs Production Systems
  • Marc Brooker, “Timeouts, Retries, and Backoff with Jitter,” Amazon Builders’ Library
  • Martin Fowler, “CircuitBreaker,” martinfowler.com
  • Nassim Nicholas Taleb, Antifragile: Things That Gain from Disorder