Saltar al contenido

Error Boundaries: cuando algo se rompe

Un componente hondo lanza una excepción. ¿Qué hace React?

Al terminar vas a poder explicar

  • Describir el unwinding del árbol paso a paso
  • Explicar por qué getDerivedStateFromError y componentDidCatch están separados
  • Decidir dónde poner los límites para que un error no se lleve toda la pantalla

Un componente hondo en el árbol lanza una excepción durante el render. ¿Qué hace React?

Lo mismo que hace una excepción de JavaScript: sube. Pero no sube por el stack de llamadas — sube por la cadena de punteros return del árbol de Fibers, buscando quién sepa atraparla.

El unwinding, paso a paso

Un error subiendo hasta el Error Boundary

AGENDADOpaso 1 / 142
Traza
Cadena de hooks · <>

Sin hooks en este paso.

Árbol de fibers

  • <HostRoot>

Salida

Nada en el host todavía. React no commiteó ningún cambio.

createRoot().render()

Montaje inicial. No hay árbol previo, así que todo se va a crear desde cero.

VER EL CÓDIGO QUE SE ESTÁ TRAZANDO
1class LimiteDeError extends React.Component {2  state = { error: null };3 4  static getDerivedStateFromError(error) {5    return { error };            // fase render: sólo devolver estado6  }7 8  componentDidCatch(error, info) {9    reportar(error, info);       // fase commit: acá sí van los efectos10  }11 12  render() {13    if (this.state.error) return <Respaldo error={this.state.error} />;14    return this.props.children;15  }16}17 18<LimiteDeError>19  <Tarjeta>20    <Riesgoso />                 {/* si esto lanza, sube hasta el límite */}21  </Tarjeta>22</LimiteDeError>

Con el foco acá: ← → un paso · espacio reproduce · Inicio/Fin van a los extremos.

Buscá los pasos unwinding y atrapa el error. React recorre hacia arriba y va descartando el trabajo a medio hacer de cada Fiber que atraviesa. Nada de eso llega al commit: el subárbol entero se pierde, con sus hooks y su estado.

Cuando encuentra el Error Boundary, marca ese Fiber con el error, tira sus hijos y vuelve a renderizar desde ahí — esta vez con la interfaz de respaldo.

Por qué siguen siendo clases

Es la única cosa en React moderno que todavía necesita una clase, y no es por pereza del equipo: es porque hacen falta dos ganchos en dos fases distintas.

React real

Un Error Boundary de verdad

En React todavía se escriben con clases: son los únicos componentes con getDerivedStateFromError.

Código

1let disparar = null;2function accion() { if (disparar) disparar(); }3 4class LimiteDeError extends React.Component {5  state = { error: null };6 7  static getDerivedStateFromError(error) {8    return { error };          // fase render: sólo devolver estado9  }10 11  componentDidCatch(error, info) {12    console.log("reportar al servicio de errores:", error.message);13    console.log("pila de componentes:", info.componentStack.trim().split("\n")[1]);14  }15 16  render() {17    if (this.state.error) {18      return <p style={{ color: "#ff7b93" }}>Se rompió: {this.state.error.message}</p>;19    }20    return this.props.children;21  }22}23 24function Hijo({ roto }) {25  if (roto) throw new Error("no pude leer los datos");26  return <p>Todo bien por acá.</p>;27}28 29function App() {30  const [roto, setRoto] = useState(false);31  useEffect(() => { disparar = () => setRoto(true); });32 33  return (34    <LimiteDeError>35      <Hijo roto={roto} />36      <button onClick={() => disparar()}>romper el hijo</button>37    </LimiteDeError>38  );39}

Corriendo de verdad

bajando React 19.2.0 de esm.sh…

Registro0 commits

Todo lo que imprimas con console.log aparece acá. Los commits los reporta el Profiler de React de verdad.

  • static getDerivedStateFromError(error) corre en la fase render. Tiene que ser pura: sólo devuelve el estado nuevo. Nada de efectos, porque esta fase se puede repetir o descartar.
  • componentDidCatch(error, info) corre en el commit. Acá van los efectos: reportar al servicio de errores, registrar, avisar. Y acá tenés el componentStack, que te dice por qué componentes pasó.

Un hook no puede expresar esa división. Hubo propuestas, pero ninguna resultó tan clara como dos métodos con nombres distintos en dos fases distintas.

Qué NO atrapan

Esto sorprende a mucha gente y es la causa de la mitad de los errores que se escapan:

  • Manejadores de eventos. Corren fuera del render, así que no hay nada que desenrollar. Usá try/catch.
  • Código asincrónico. Un setTimeout o un .then escapan del stack de React por completo.
  • Errores del propio boundary. Suben al siguiente boundary hacia arriba.
  • Renderizado del lado del servidor. Ahí el manejo es distinto.

La regla es simple: un Error Boundary atrapa lo que ocurre durante el render, el commit o los constructores de sus descendientes. Todo lo demás es tuyo.

Dónde ponerlos

Si no hay ninguno, React desmonta la aplicación entera. Eso parece brutal y es deliberado: una interfaz mostrando datos corruptos o un carrito con el precio equivocado es peor que una pantalla en blanco.

1// Uno arriba de todo: la red de seguridad. Nunca debería activarse.2<ErrorBoundary respaldo={<PantallaDeError />}>3 4  {/* Uno por región independiente: si el panel de ventas se rompe,5      el resto del tablero sigue funcionando. */}6  <ErrorBoundary respaldo={<PanelRoto nombre="Ventas" />}>7    <PanelVentas />8  </ErrorBoundary>9 10  <ErrorBoundary respaldo={<PanelRoto nombre="Usuarios" />}>11    <PanelUsuarios />12  </ErrorBoundary>13 14</ErrorBoundary>

El criterio: un boundary por cada región que pueda fallar independientemente. Si dos partes de la pantalla no tienen nada que ver entre sí, no tienen por qué caerse juntas.

Antes de seguir · predecí

Un botón hace fetch en su onClick y el servidor devuelve 500. El Error Boundary no muestra nada. ¿Por qué?

Lo que te llevás

El error sube por los punteros return haciendo unwinding y descartando el trabajo. El Error Boundary lo atrapa y renderiza el respaldo. Siguen siendo clases porque hace falta un gancho puro en la fase render y otro con efectos en el commit. Y sólo atrapan lo que pasa durante el render: los eventos y lo asincrónico son tuyos.

Antes de marcarla, comprobá

  • ¿Podés explicar describir el unwinding del árbol paso a paso?
  • ¿Podés explicar explicar por qué getDerivedStateFromError y componentDidCatch están separados?
  • ¿Podés explicar decidir dónde poner los límites para que un error no se lleve toda la pantalla?