facebook/react · error · Error

409

409

Error message

Cannot update an unmounted root.

What it means

Root objects returned by createRoot/hydrateRoot keep their FiberRoot in _internalRoot, which root.unmount() sets to null. Every subsequent root.render(...) reads that field first and throws error 409 when it is null, because React cannot revive an unmounted root. The usual cause is code holding a stale root reference and using it after cleanup.

Source

Thrown at packages/react-dom/src/client/ReactDOMRoot.js:112

  defaultOnUncaughtError,
  defaultOnCaughtError,
  defaultOnRecoverableError,
} from 'react-reconciler/src/ReactFiberReconciler';
import {defaultOnDefaultTransitionIndicator} from './ReactDOMDefaultTransitionIndicator';
import {ConcurrentRoot} from 'react-reconciler/src/ReactRootTags';

// $FlowFixMe[missing-this-annot]
function ReactDOMRoot(internalRoot: FiberRoot) {
  this._internalRoot = internalRoot;
}

// $FlowFixMe[prop-missing] found when upgrading Flow
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
  // $FlowFixMe[missing-this-annot]
  function (children: ReactNodeList): void {
    const root = this._internalRoot;
    if (root === null) {
      throw new Error('Cannot update an unmounted root.');
    }

    if (__DEV__) {
      // using a reference to `arguments` bails out of GCC optimizations which affect function arity
      const args = arguments;
      if (typeof args[1] === 'function') {
        console.error(
          'does not support the second callback argument. ' +
            'To execute a side effect after rendering, declare it in a component body with useEffect().',
        );
      } else if (isValidContainer(args[1])) {
        console.error(
          'You passed a container to the second argument of root.render(...). ' +
            "You don't need to pass it again since you already passed it to create the root.",
        );
      } else if (typeof args[1] !== 'undefined') {
        console.error(
          'You passed a second argument to root.render(...) but it only accepts ' +

View on GitHub (pinned to eafeac097b)

Solutions

  1. Track unmount state in a wrapper (set root = null on unmount) and check it before every render
  2. Stop timers, listeners, and subscriptions that may call render after unmount (clearInterval, AbortController)
  3. If you must remount, create a fresh createRoot(container) instead of reusing the old root
  4. In tests, await pending async work before unmounting in cleanup

Example fix

// before
root.unmount();
// later...
root.render(<App />); // throws

// after
root.unmount();
root = null;
// later...
if (root) {
  root.render(<App />);
} else {
  root = createRoot(container);
  root.render(<App />);
}
Defensive patterns

Strategy: validation

Validate before calling

class AppHost {
  root = null;
  mount(container, element) {
    if (!this.root) this.root = createRoot(container);
    this.root.render(element);
  }
  update(element) {
    if (this.root === null) return; // unmounted — skip instead of throwing
    this.root.render(element);
  }
  unmount() {
    this.root?.unmount();
    this.root = null;
  }
}

Prevention

When it happens

Trigger: Calling root.render() or root.unmount() again after root.unmount() — an event handler, interval, or pending promise that still fires after the app was torn down; double-invoked effect cleanup that unmounts while an async callback later calls root.render.

Common situations: Micro-frontend mount/unmount lifecycles where a global update function keeps running; tests that unmount in afterEach while pending work later renders; polling or subscription code capturing the root.

Related errors


AI-assisted analysis of facebook/react@eafeac097b (2026-08-21). Data as JSON: /api/errors/7b6ca9af79f52611. Report an issue: GitHub.