ErrorBoundary: contener errores de render y efectos
ErrorBoundary atrapa los errores lanzados al renderizar y en los efectos de sus descendientes y muestra un fallback, con acceso al error y a un reset; que captura y que no, los primitivos catchError y onError, y el camino de los errores de los recursos async.
Qué captura y qué no
ErrorBoundary intercepta los errores que se lanzan de forma síncrona dentro del grafo reactivo que cuelga de él: al ejecutar el cuerpo de un componente descendiente, al construir su JSX, y dentro de sus efectos y de onMount. También recibe el rechazo de un createResource cuando su valor se lee en el render, porque ese rechazo se reinyecta en el grafo.
Lo que no captura es igual de importante y es la fuente número uno de sorpresas: los errores lanzados en manejadores de eventos y en callbacks asíncronos sueltos —setTimeout, un .then a pelo— ocurren fuera de la ejecución síncrona del owner, así que escapan del límite.
| Origen del error | Lo captura |
|---|---|
| cuerpo del componente al renderizar | sí |
| JSX y su evaluación inicial | sí |
createEffect, createRenderEffect, onMount |
sí |
rechazo de createResource leído en el render |
sí |
manejador onClick u otro evento |
no |
setTimeout, .then, callbacks async sueltos |
no |
import { ErrorBoundary } from "solid-js";<ErrorBoundary fallback={(err, reset) => ( <div role="alert"> <p>Se rompió algo: {err.message}</p> <button onClick={reset}>Reintentar</button> </div> )}> <Panel /></ErrorBoundary>;El fallback: error y reset
El fallback admite dos formas. Como elemento fijo —fallback={<Caido />}— para un mensaje estático. O, mucho más útil, como función que recibe dos argumentos: el error capturado y un reset. El error es el objeto lanzado tal cual; léelo para clasificar y mostrar. El reset es una función que vuelve a montar los hijos del límite desde cero: descarta el estado de error y reintenta el render original.
La sutileza de reset es que no arregla nada por sí mismo. Si vuelves a renderizar y la causa persiste —el servidor sigue caído, el dato sigue siendo inválido— el error se relanza de inmediato y regresas al fallback. reset sirve cuando la causa fue transitoria o cuando algo cambió entre medias: reintentar una petición, releer un recurso que ya refrescaste, dar otra oportunidad tras una acción del usuario.
Un matiz de implementación: la función fallback corre dentro del owner del límite, así que puede crear su propia reactividad —un signal para contar intentos, un createEffect que reintente sola tras un tiempo—. El reset no es un botón obligatorio: es una función que disparas desde donde quieras, incluida una lógica de recuperación automática.
El límite es jerárquico: un error sube por el árbol de owners hasta el ErrorBoundary más cercano, igual que una excepción busca el catch más interno. Si no hay ninguno, el error llega a la raíz y tumba la aplicación. De ahí que convenga poner límites granulares, tema del último nivel de este bloque.
catchError y onError
Bajo ErrorBoundary hay dos primitivos que puedes usar directamente. catchError envuelve una computación y desvía su error a un manejador, sin UI de por medio:
import { catchError } from "solid-js";const valor = catchError( () => calcularArriesgado(), (err) => registrar(err), // se ejecuta si calcularArriesgado lanza);onError registra un manejador en el owner actual que corre cuando un descendiente lanza, durante la propagación, antes de que el error alcance un límite superior. Es ideal para efectos de registro —telemetría, logging— sin construir un fallback:
import { onError } from "solid-js";function ConTelemetria(props: { children: JSX.Element }) { onError((err) => enviarASentry(err)); // observa, no detiene la propagacion return <>{props.children}</>;}onError no consume el error: lo observa y deja que siga subiendo hasta el ErrorBoundary que lo mostrará. Separar así el registro —onError— de la presentación —fallback— es el patrón limpio para instrumentar sin ensuciar la UI.
Para el error que sí nace en un evento, el patrón de enrutado es guardarlo en un signal y releerlo lanzándolo en el render, de modo que el fallo reaparezca dentro del grafo:
const [fatal, setFatal] = createSignal<Error>();// leer esto en el render relanza el error donde ErrorBoundary lo capturaconst guardia = () => { const e = fatal(); if (e) throw e; };const alPulsar = async () => { try { await accionRiesgosa(); } catch (e) { setFatal(e as Error); } // el evento no puede lanzar al limite, pero un signal si};Errores async con recursos
El camino idiomático para que un fallo asíncrono llegue al límite es createResource. Un recurso toma una promesa y expone su estado —cargando, listo, con error— como algo reactivo; si la promesa se rechaza, leer el recurso en el render relanza ese error dentro del grafo, justo donde ErrorBoundary puede verlo. Combinado con Suspense para el estado de carga, cubre los tres estados sin un solo if manual:
import { createResource, Suspense, ErrorBoundary } from "solid-js";function Perfil(props: { id: string }) { const [usuario] = createResource(() => props.id, cargarUsuario); return ( <ErrorBoundary fallback={(err, reset) => ( <button onClick={reset}>Reintentar tras {err.message}</button> )} > <Suspense fallback={<Cargando />}> <h1>{usuario()?.nombre}</h1> </Suspense> </ErrorBoundary> );}Suspense gobierna el “todavía no”; ErrorBoundary gobierna el “salió mal”. Son ejes ortogonales: uno espera, el otro contiene. Colocar el ErrorBoundary por fuera del Suspense es lo habitual, para que un fallo de carga sustituya toda la región en lugar de quedar atrapado bajo el indicador de progreso.
Este es también el vehículo para reintentar datos: el segundo elemento que devuelve createResource trae un refetch, y llamarlo relanza la carga. Un reset del límite tras un refetch reintenta el render sobre datos frescos —el patrón que el último nivel de este bloque convierte en una estrategia de recuperación completa.