wandres.dev
DYNAMIC, PORTAL, ERRORBOUNDARY · componentes especiales

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.

⏱ 15 min

En un sistema de grano fino un error no atajado es destructivo: lanzado al crear un nodo o al correr un efecto, derriba el subárbol reactivo y deja la pantalla a medias. ErrorBoundary acota el radio de la explosión. Envuelve una región, captura los errores que broten al renderizarla o en sus efectos, y en su lugar muestra un fallback —opcionalmente con el error y con un reset para reintentar—. Es el try/catch del árbol de componentes.

🎯 Al terminar esta lección sabrás
  • Capturar con ErrorBoundary los errores de render y de efectos de los descendientes.
  • Escribir un fallback como función (err, reset) con acceso al error y al reintento.
  • Distinguir con precisión qué captura el límite y qué se le escapa.
  • Conocer los primitivos catchError y onError y el camino 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 sueltossetTimeout, 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
JSX y su evaluación inicial
createEffect, createRenderEffect, onMount
rechazo de createResource leído en el render
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.

flowchart TD
ERR[Error al renderizar o en un efecto] --> PROP[sube por el arbol de owners]
PROP --> EB[ErrorBoundary mas cercano]
EB --> FB[muestra el fallback con err y reset]
FB --> RST[reset vuelve a montar los hijos]
RST --> PROP
style EB fill:#f38ba8,color:#11111b
style RST fill:#a6e3a1,color:#11111b

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 registroonError— de la presentaciónfallback— 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 captura
const 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.

⚠️
Los errores de eventos y async no suben solos

Un throw dentro de un onClick no llega a ErrorBoundary. Para enrutarlo, captura el error en el manejador y provócalo dentro del grafo: guarda el error en un signal y léelo —lanzándolo— en el render, o convierte la operación en un createResource, cuyo rechazo sí baja al límite. Lo mismo vale para un await suelto: si su fallo debe pintar fallback, hazlo pasar por un recurso o por una señal que el render observe.

ErrorBoundary es el try/catch del grafo, no del código

La clave para dominar ErrorBoundary es aceptar que su unidad de captura no son líneas de código sino nodos del grafo reactivo. Un try/catch de JavaScript rodea una ejecución en el tiempo; ErrorBoundary rodea una región del árbol de owners en el espacio. Por eso captura exactamente lo que se ejecuta como parte de ese árbol —el cuerpo del componente, el JSX, los efectos, onMount, el rechazo de un recurso leído en render— y por eso deja escapar lo que se ejecuta fuera de él: un manejador de evento corre en respuesta a un clic, no como parte de la construcción del árbol, y un setTimeout dispara en un tick futuro sin owner que lo contenga. Interiorizar esta frontera —dentro del grafo se captura, fuera del grafo no— disuelve casi todas las confusiones. Y revela la técnica maestra: para que un error async o de evento sea capturable, hay que devolverlo al grafo, y el vehículo canónico es createResource, que toma una promesa y la reinyecta como estado reactivo, con su rechazo viajando limpio hasta el fallback. Los errores no se atrapan donde se lanzan, sino donde el grafo los vuelve a tocar.

⚔️ Contén y reintenta
  1. Envuelve un componente que lanza al renderizar en un ErrorBoundary con fallback de función; muestra err.message y un botón de reset.
  2. Haz que el fallo sea transitorio —falla las dos primeras veces, luego funciona— y comprueba que reset acaba recuperándose.
  3. Lanza un error dentro de un onClick y verifica que el límite no lo captura; después enrútalo por un signal leído en el render.
  4. Convierte esa misma operación en un createResource y confirma que su rechazo sí llega al fallback.
  5. Añade onError en un ancestro para registrar el error sin sustituir el fallback; observa que ambos se ejecutan.