Saltar al contenido
Plazoleta
· Raúl López

El fallo seguro se elige antes de que falle

En la web de Plazoleta hay tres sistemas que deciden en qué lado caer cuando algo falla: el blog no publica, la analítica no mide y el formulario no dice "gracias". No es pesimismo: es elegir el lado antes de que haga falta.

El formulario de contacto de esta web tiene una regla escrita en la cabecera del fichero que lo gestiona: solo se responde "gracias" si el correo ha salido de verdad. Si Resend falla, si falta la configuración, si el envío se queda a medias, la respuesta es un error que incluye la dirección para escribir directamente.

En una web cuyo objetivo es que la gente escriba, fallar a la vista parece un mal negocio. La alternativa es más cómoda: decir "gracias" siempre y, si el aviso no llegó, confiar en que la persona insista. Lo que pasa es que nadie insiste: cree que ya te ha escrito. Un lead perdido en silencio no se ve, no se mide y no se arregla. Es la clase de fallo que descubres cuando el negocio lleva meses plano y alguien pregunta por qué.

Todo sistema falla. La pregunta que decide el daño no es si va a fallar, sino qué hace cuando falle. Y cuando llega ese momento ya es tarde para decidir: el lado al que cae estaba escrito antes, en el valor por defecto de un campo, en el catch que alguien añadió para que el error dejara de molestar.

Tres sitios con el lado ya elegido

El post que no se publica

El blog de esta web se publica solo, sin revisión humana entre el texto y producción. Por eso src/lib/blog.ts no deja nada al despiste: el propio código decide qué puede publicarse.

La primera pieza es el estado. Si un post no lleva estado, no se publica:

// El estado por defecto es "borrador" a propósito: si a alguien se le olvida
// ponerlo, el post NO se publica. El fallo seguro es no publicar.
const estado: PostEstado = data.estado === "publicado" ? "publicado" : "borrador";

Las otras dos no son tan silenciosas: un post marcado como publicado cuya fecha no sea un YYYY-MM-DD válido, o cuyo texto contenga algo con la forma de una credencial, revienta el build. El fallo seguro vuelve a ser no publicar, y aquí es ruidoso a propósito: si el que falla es el despliegue, te enteras ese mismo día.

Hay una asimetría que merece la pena mirar. Un borrador no aparece en el índice, ni en el sitemap, ni por URL directa. El modo "no publicado" no es un aviso de error: es el estado por defecto del sistema. El error no es que algo se quede fuera; el error sería que algo entre sin cumplir las reglas.

La analítica que no mide

src/lib/analytics.ts aplica la misma pregunta a la privacidad. Sin identificador de medición no hay banner, ni script, ni eventos: la web se comporta exactamente como antes de que existiera la analítica. La condición no es "cargar y luego preguntar", es "no cargar hasta que haya un sí".

Pero la parte interesante está en el caso raro: cuando localStorage lanza una excepción.

try {
  const valor = window.localStorage.getItem(CLAVE_CONSENTIMIENTO);
  return valor === "aceptado" || valor === "rechazado" ? valor : "sin-decidir";
} catch {
  // Storage bloqueado (navegación privada estricta): se trata como sin decidir
  // y no se mide. Ante la duda, no medir.
  return "sin-decidir";
}

Con el almacenamiento bloqueado no se puede saber si la persona dijo que no o si nunca llegó a decidir, así que se trata como "sin decidir" y no se mide. Consecuencia: el banner vuelve a salir en cada carga. Es una molestia pequeña, elegida frente a la alternativa —medir a quien no ha podido decir que sí—, que no tiene arreglo después.

Y el coste está escrito al lado: de quien rechaza no se sabrá nada (en España, habitualmente entre un 30 % y un 50 % del tráfico), así que los números de GA4 son un suelo, no la realidad. Medir menos es una decisión, no un accidente.

También hay una defensa pequeña y muy útil: evento() no hace nada si no existe gtag. Los componentes pueden llamar a la analítica sin preguntar antes, y un if olvidado no puede convertirse en un evento enviado sin permiso. El fallo seguro está en el único sitio por el que pasan todos los eventos.

El correo que no tumba el envío

Volvemos al formulario, porque ahí hay dos correos y dos lados distintos.

El aviso interno —el que lleva el mensaje y el email de la persona— es lo que importa. Si falla, la respuesta al usuario es un error con la dirección de contacto, así que el lead no se pierde: la persona sabe que tiene que escribir. Es el lado conservador, no prometer lo que no ha pasado.

La confirmación al remitente es distinta. Sale después, y si falla se registra y punto: la respuesta sigue siendo "gracias".

/**
 * Confirmación al remitente en su idioma. Deliberadamente NO devuelve error:
 * el correo que importa (el aviso con el lead) ya salió; si esta cortesía
 * falla, se registra y la respuesta al usuario sigue siendo "gracias".
 */

Dos correos, dos políticas opuestas, y las dos son correctas: el lado seguro no se elige por el tipo de operación, se elige por lo que se pierde si falla. En el primero, un lead. En el segundo, un email de cortesía que ya no aporta nada porque el mensaje original está en el buzón.

La pregunta, hecha a tiempo

Visto así parece obvio, y no lo es: la mayoría de los catch que he escrito no decidían nada, tapaban. La diferencia entre los tres casos de arriba y un catch { return ok } no es la técnica, es que alguien hiciera la pregunta antes de escribir el código: cuando esto falle, ¿en qué lado quiero caer?

La respuesta cambia según lo que esté en juego:

  • ¿El fallo destruye algo que no se puede deshacer? El post publicado, el dato enviado a Google, el "gracias" que crea una expectativa falsa. Ahí se para: fallar cerrado, aunque duela.
  • ¿El fallo se puede deshacer o no invalida nada? La confirmación que no sale, el banner que reaparece, el error visible que invita a reintentar. Ahí se sigue, pero se registra.
  • ¿El fallo es silencioso? Es la única respuesta que nunca vale. Un sistema que falla y dice que todo ha ido bien no es tolerante a fallos: es mentiroso. El usuario no reintenta, el log no existe, y el problema aparece meses después, cuando ya no se puede reconstruir qué pasó.

Hay un cuarto caso que conviene reconocer porque es el más incómodo: cuando las dos opciones cuestan. Si el banner reaparece, molestas; si mides sin consentimiento, incumples. No hay un lado gratis: hay un lado que puedes defender y otro que no. "Ante la duda, no medir" es una decisión de producto, no una nota técnica. Y está escrita donde se lee el catch, que es justo donde hay que leerla.

Lo que nos llevamos

Un valor por defecto es una política de fallo. El estado que cae en borrador, el "sin decidir" del storage, el error con dirección de contacto: los tres se escribieron antes de que fallara nada, y los tres deciden qué pasa cuando falle. Si no lo decides tú, lo decide el camino más cómodo, que casi nunca es el lado seguro.

El fallo seguro casi nunca es "seguir". Y cuando lo es, es porque el fallo es visible y barato. Lo que no se sostiene jamás es el éxito falso: decir "gracias" sin enviar, medir sin permiso, publicar sin que el build valide.

El coste hay que escribirlo al lado. La analítica pierde a quien rechaza, el blog no publica un post con un despiste, el formulario muestra un error donde podría poner una sonrisa. Son precios reales, y si no aparecen escritos, el siguiente que pase por ahí los va a "arreglar".

La pregunta completa —cuando esto falle, ¿en qué lado quiero caer?— toma treinta segundos y se responde mejor con el sistema en verde que con el incidente abierto. Es de las pocas decisiones que salen gratis precisamente porque se toman antes.

Si tu sistema falla en silencio y quieres revisar de qué lado está cayendo, escríbeme.

¿Tienes un problema parecido?

Cuéntanoslo. Hablas directamente con quien escribe el código.