wandres.dev
DYNAMIC, PORTAL, ERRORBOUNDARY · componentes especiales

Patrones robustos: límites, logging y reintento

Industrializar la resiliencia con ErrorBoundary: limites de error por seccion para aislar fallos, logging centralizado con onError, y recuperacion con reset combinado con refetch de recursos, backoff exponencial y freno a los bucles de reintento.

⏱ 16 min

Capturar un error es el principio; sobrevivirlo con dignidad es la meta. Este nivel industrializa la resiliencia: límites por sección para que el fallo de un widget no borre la página entera, logging centralizado que registra sin ensuciar la UI, y recuperación que combina el reset del límite con el refetch del recurso, con backoff y un freno para no caer en bucles de reintento infinito.

🎯 Al terminar esta lección sabrás
  • Aislar fallos con un ErrorBoundary por sección independiente.
  • Centralizar el registro de errores con onError sin acoplarlo al fallback.
  • Recuperar combinando reset con refetch de un createResource.
  • Frenar los reintentos con backoff exponencial y un tope de intentos.

Límites por sección

Un único ErrorBoundary en la raíz convierte cualquier error en una pantalla en blanco. La estrategia robusta es la contraria: muchos límites pequeños, uno por región independiente —la barra lateral, el feed, cada tarjeta— de modo que un fallo se contenga y el resto siga vivo. Un helper Seccion lo hace repetible:

import { ErrorBoundary } from "solid-js";

function Seccion(props: { nombre: string; children: JSX.Element }) {
  return (
    <ErrorBoundary
      fallback={(err, reset) => (
        <div role="alert" class="seccion-caida">
          <p>La sección {props.nombre} no se pudo cargar.</p>
          <button onClick={reset}>Reintentar</button>
        </div>
      )}
    >
      {props.children}
    </ErrorBoundary>
  );
}

// si Feed falla, Barra y Perfil siguen en pie
<>
  <Seccion nombre="barra"><Barra /></Seccion>
  <Seccion nombre="feed"><Feed /></Seccion>
  <Seccion nombre="perfil"><Perfil /></Seccion>
</>;

La granularidad es una decisión de diseño: demasiado gruesa y un fallo tira de más; demasiado fina y llenas la interfaz de mensajes de error. La regla útil es un límite por unidad de valor independiente para el usuario.

Logging: onError junto al fallback

El fallback presenta el error al usuario; el registro para el equipo es otra responsabilidad, y conviene separarla. onError capta el error durante su propagación y lo envía a tu telemetría sin decidir nada de la UI:

import { onError } from "solid-js";

function ConRegistro(props: { area: string; children: JSX.Element }) {
  onError((err) => {
    reportar({ area: props.area, mensaje: String(err), cuando: Date.now() });
  });
  return <>{props.children}</>;
}

// se compone con el limite: registra y ademas muestra fallback
<Seccion nombre="feed">
  <ConRegistro area="feed"><Feed /></ConRegistro>
</Seccion>;

Como onError no detiene la propagación, el error sigue subiendo hasta el ErrorBoundary de la Seccion, que pinta el fallback. Registro y presentación quedan desacoplados y componibles: puedes envolver con ConRegistro cualquier parte, con o sin límite propio.

Recuperación: reset, refetch y backoff

reset vuelve a montar los hijos, pero si la causa persiste el error reaparece al instante. Para una recuperación real necesitas cambiar algo entre el fallo y el reintento. Con datos async, ese algo es refetch: disparas una nueva carga del recurso y después llamas a reset para que el render vuelva a intentarse sobre datos frescos.

const [datos, { refetch }] = createResource(fuente, cargar);

<ErrorBoundary
  fallback={(err, reset) => (
    <Fallo
      mensaje={err.message}
      onReintentar={() => {
        refetch(); // nueva peticion
        reset();   // y re-render del subarbol sobre el recurso recargado
      }}
    />
  )}
>
  <Vista datos={datos()} />
</ErrorBoundary>;

Falta el freno. Un reintento que siempre falla y siempre se reintenta es un bucle. La defensa es un tope de intentos con backoff exponencial: cuenta los reintentos en un signal del owner —sobrevive a los reset—, espacia cada reintento y ríndete al superar el máximo.

import { ErrorBoundary, Show, createSignal } from "solid-js";

function ConReintentos(props: { max?: number; children: JSX.Element }) {
  const max = props.max ?? 3;
  const [intentos, setIntentos] = createSignal(0);
  return (
    <ErrorBoundary
      fallback={(err, reset) => (
        <div role="alert">
          <p>{err.message}</p>
          <Show
            when={intentos() < max}
            fallback={<p>Se agotaron los reintentos. Contacta con soporte.</p>}
          >
            <button
              onClick={() => {
                setIntentos((n) => n + 1);
                setTimeout(reset, 2 ** intentos() * 200); // 200, 400, 800 ms
              }}
            >
              Reintentar {intentos()} de {max}
            </button>
          </Show>
        </div>
      )}
    >
      {props.children}
    </ErrorBoundary>
  );
}
flowchart LR
CARGA[Cargando] --> OK[Listo]
CARGA --> FALLO[Fallido y capturado]
FALLO -->|reset con backoff| CARGA
FALLO -->|supera el maximo| FIN[Agotado y escalado]
style FALLO fill:#f38ba8,color:#11111b
style OK fill:#a6e3a1,color:#11111b
ℹ️
Reset por clave para forzar un remontaje limpio

Cuando el estado corrupto vive dentro del subárbol y reset no basta, fuerza un remontaje cambiando una clave: envuelve la región en un Show con keyed cuya condición sea un signal que incrementas al reintentar. Cambiar la clave desecha el subárbol entero y lo recrea sin rastro del estado anterior, más agresivo que reset pero infalible contra estados internos pegajosos.

La resiliencia es composición de límites, no un manejador global

El error de diseño más común es pensar la gestión de errores como un punto único —un gran try/catch en la raíz, un manejador global— cuando en Solid es una propiedad emergente de cómo compones los límites. Cada ErrorBoundary es una celda de contención con tres responsabilidades que conviene mantener separadas y componibles: aislar —contener el fallo a una sección para que el resto de la aplicación siga sirviendo valor—, observar —registrar con onError lo ocurrido para el equipo, sin mezclarlo con lo que ve el usuario— y recuperar —ofrecer un camino de vuelta con reset, nutrido por un refetch que cambie la realidad y frenado por un backoff que impida el bucle—. La madurez consiste en calibrar la granularidad: un límite por unidad de valor independiente, ni uno solo que lo apague todo ni tantos que fragmenten la interfaz en avisos. Y en distinguir el error transitorio, que merece reintento automático, del fatal, que debe escalar y detenerse. Cuando interiorizas que resiliencia no es atrapar todos los errores en un sitio sino diseñar dónde viven las fronteras, tus aplicaciones dejan de caerse enteras: se degradan por partes, se recuperan solas cuando pueden y piden ayuda cuando no.

⚔️ Diseña la contención
  1. Envuelve tres regiones en su propio Seccion; haz fallar una y comprueba que las otras dos siguen funcionando.
  2. Añade ConRegistro alrededor de una de ellas y verifica que el error se registra y se muestra el fallback.
  3. Combina un createResource con un límite: en el fallback, reintenta con refetch seguido de reset y observa la recuperación sobre datos frescos.
  4. Implementa ConReintentos con tope y backoff; comprueba que tras el máximo deja de ofrecer el botón y muestra el mensaje final.
  5. Fuerza un remontaje por clave con un Show keyed y contrasta su efecto con el de un reset simple sobre un estado interno corrupto.