skip to content

In a React class error boundary, what is the difference between static getDerivedStateFromError(error) and componentDidCatch(error, errorInfo), and what belongs in each?

level: middleimportance: must knowfreq 66%

answer

  1. one derives state, one has side effects
  2. render phase versus commit phase
  3. static means no this, must be pure
  4. componentStack lives on the second argument
  5. render can replay, so no logging there

basics

~20 s

getDerivedStateFromError is a static, pure, render-phase method that returns a state patch so the boundary renders its fallback. componentDidCatch runs after the commit and is where side effects such as logging belong. Most boundaries implement both.

solid answer

~50 s

They run in different phases and have different rules. `static getDerivedStateFromError(error)` is called during the render phase, while React is retrying the failed render; it must be pure and its only job is to return a state patch — typically `{ error }` — so the next render shows the fallback. Because it is static it has no `this`, and because it runs in render React may call it more than once for the same error, so no logging or network calls belong there. `componentDidCatch(error, errorInfo)` runs after React has committed the fallback, so side effects are allowed: this is where you report to Sentry or your own endpoint, using `errorInfo.componentStack` to see which component threw. It also never runs during server rendering, since nothing commits on the server. In React 19 you can additionally centralise reporting with the `onCaughtError` option on `createRoot`.

code

jsx · 22 lines
jsx
import { Component } from 'react';

export class ErrorBoundary extends Component {
  state = { error: null };

  static getDerivedStateFromError(error) {
    // Render phase: pure, no side effects, no `this`.
    return { error };
  }

  componentDidCatch(error, errorInfo) {
    // Commit phase: side effects are allowed here.
    console.error(error.message, errorInfo.componentStack);
  }

  render() {
    if (this.state.error) {
      return <p role="alert">{this.props.label} failed to load.</p>;
    }
    return this.props.children;
  }
}

go deeper

for a junior

Recall that a class boundary defines two methods: one returns state so a fallback renders, the other receives the error for logging. Being able to name both and say which one logs is enough here.

for a middle

Explain the phase each belongs to and the rule it implies — pure and static during render, side-effect-safe after commit — and mention errorInfo.componentStack as the thing worth sending to your logger.

for a senior

Show you have operated this: minified stacks make componentStack the useful signal, duplicate reports come from logging in the render path, and React 19's onCaughtError root option removes copy-pasted logging from every boundary.

for a principal

Frame it as an instance of React's render/commit contract: purity in the replayable phase is what makes concurrent rendering possible, and any API that mixes derivation with effects becomes a hazard the moment renders can be discarded.

## Two methods, two phases A class becomes an error boundary by defining `static getDerivedStateFromError`, `componentDidCatch`, or both. They are not alternatives that do the same thing in different styles; they sit on opposite sides of React's render/commit split, and that split dictates what each may do. React does its work in two phases. The **render phase** calls your components to compute the next tree; it can be paused, abandoned, or replayed, so everything in it must be pure. The **commit phase** applies the result to the DOM and runs the effects; it happens once per committed update, so side effects are safe there. `getDerivedStateFromError` belongs to the first, `componentDidCatch` to the second. ## static getDerivedStateFromError(error) When a descendant throws, React unwinds to the nearest boundary and re-renders it, calling this static method with the thrown value. Whatever object it returns is merged into the boundary's state, exactly like the return value of `setState`. A boundary that returns `{ error }` can then check that state in `render` and return fallback UI. ```jsx static getDerivedStateFromError(error) { return { error }; } render() { if (this.state.error) return <Fallback error={this.state.error} />; return this.props.children; } ``` Three consequences of it being a *static, render-phase* method: 1. **No `this`.** It cannot read props or instance fields, and it cannot call instance methods. That is intentional: React calls it in the middle of recovering, before the instance is trusted to be in a consistent state. 2. **It must be pure.** React may re-run the failing render — including this method — as part of recovery and, in development, `<StrictMode>` amplifies re-invocation further. Logging from here can fire twice for one error. 3. **It runs on the server too.** Since it is part of rendering, it participates wherever rendering happens. The method only decides *what state the boundary should be in*. It does not stop the unmounting of children, and it does not decide what the fallback looks like — that is `render`'s job. ## componentDidCatch(error, errorInfo) This one is an instance method, called after React has committed the boundary's new output. Because the commit has already happened, side effects are legitimate — and this is the only one of the two where they are. ```jsx componentDidCatch(error, errorInfo) { logToServer({ message: error.message, stack: error.stack, componentStack: errorInfo.componentStack, }); } ``` `errorInfo.componentStack` is the piece you cannot get anywhere else: a human-readable list of the React components between the throwing component and the boundary. A raw JavaScript stack trace after a production build points at minified bundle frames; the component stack tells you it was `<PriceChart>` inside `<Dashboard>`, which is usually what makes the report actionable. Because it runs in the commit phase, it never fires during server rendering — there is no commit on the server. You *can* call `this.setState` inside `componentDidCatch` and use it alone as a boundary, but the idiomatic split is: state transition in the static method, reporting in the instance method. ## Why the split exists at all Before concurrent rendering, a single lifecycle hook that both logged and set state was fine, because renders were never abandoned or replayed. Once React gained the ability to start a render, throw it away, and start again, any side effect in the render path became unsafe — it could run for work that never reaches the screen. Splitting the boundary API into a pure state-derivation function and a post-commit callback is the same refactor React applied across the whole API surface (`getDerivedStateFromProps`, the deprecation of the `UNSAFE_componentWill*` methods). ## What this looks like in React 19 Both methods are alive and unchanged in React 19; error boundaries are still class components. What React 19 adds is *centralised reporting* through root options: ```jsx createRoot(container, { onCaughtError: (error, errorInfo) => report(error, errorInfo.componentStack), onUncaughtError: (error, errorInfo) => reportFatal(error, errorInfo.componentStack), }); ``` `onCaughtError` fires for every error a boundary handled, with the same component-stack information. That does not replace `componentDidCatch` — you still need it for per-boundary context such as which feature or user flow failed — but it means you no longer have to duplicate logging code in every boundary class just to be sure nothing is missed. ## The answer an interviewer is listening for Name the phase for each, then the rule that follows from the phase: pure state derivation in the static method because render can be replayed; side effects only in `componentDidCatch` because the commit happened once. Candidates who describe them as "two ways to do the same thing" usually also put their logging in the wrong place.

  • Can you build a working error boundary with only componentDidCatch?
    Yes — you can call `this.setState({ error })` inside it and branch in `render`. It works because the commit-phase update schedules another render. The conventional split still puts the state transition in `getDerivedStateFromError`, because that is the pure, replay-safe place for it, and leaves `componentDidCatch` purely for reporting.
  • What is in errorInfo, and why does it matter more than error.stack?
    `errorInfo.componentStack` lists the React components between the throwing component and the boundary. After a production build, `error.stack` points at minified frames, while the component stack still names `<PriceChart>` inside `<Dashboard>` — which is usually what turns a report into something a developer can act on.
  • Why must getDerivedStateFromError avoid logging?
    It runs in the render phase, which React may replay while recovering from the error, and which StrictMode double-invokes in development. Anything with a side effect can therefore fire more than once per real failure, producing duplicate reports and skewing error-rate metrics. Report from `componentDidCatch` instead.

saying these in an interview costs you the question

  • Says the two methods are interchangeable styles
  • Puts the logging call in getDerivedStateFromError
  • Tries to read this.props inside the static method
  • Expects componentDidCatch to run during server rendering
  • Thinks returning state from the static method stops the unmount

context