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.
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> );}