React NotesRohit’s interview study guide
Chapter 19

Error Handling

Error boundaries and what they can't catch, react-error-boundary 6, React 19's root error callbacks, and handling errors from event handlers and async code — with React 19.3's real console output.

5 topics
Parts marked Advanced are extra depth. Skip them on a first read or a quick revision.

#Error boundaries

When a component throws while rendering, React can't produce a consistent UI for that part of the tree, so it removes it. Without anything to stop it, the error unwinds all the way up and React unmounts the entire app — the user gets a blank page. An error boundary is a component that stops that unwinding: it catches errors thrown while rendering anything below it, and renders a fallback UI instead.

From my notes: error boundaries "catch errors that occur during rendering, in lifecycle methods, and in the constructors of child components", and they "help prevent the entire application from crashing by isolating where the error occurred and rendering a fallback UI, such as an error message."

The mental model is a circuit breaker in a fuse box. A fault in one room trips that room's breaker; the lights stay on in the rest of the house. Where you put breakers decides how much goes dark: one at the root (the whole app shows "Something went wrong"), one per route, or one per widget (only the broken chart disappears).

There is still no hook for this in React 19.3: an error boundary must be a class component that defines static getDerivedStateFromError (and usually componentDidCatch). In practice most apps use react-error-boundary, which is that class, written once.

ErrorBoundary.jsx
import { Component, useState } from "react";

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

  // Render phase: turn the error into state. Must be pure.
  static getDerivedStateFromError(error) {
    return { error };
  }

  // Commit phase: side effects such as logging.
  componentDidCatch(error, info) {
    this.props.onError?.(error, info.componentStack);
  }

  render() {
    if (this.state.error) {
      return (
        <div role="alert">
          <p>Something went wrong: {this.state.error.message}</p>
          <button onClick={() => this.setState({ error: null })}>Try again</button>
        </div>
      );
    }
    return this.props.children;
  }
}

export function BuggyCounter() {
  const [count, setCount] = useState(0);
  if (count === 3) throw new Error("I crashed at 3!");
  return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>;
}

export function App({ onError }) {
  return (
    <main>
      <ErrorBoundary onError={onError}>
        <BuggyCounter />
      </ErrorBoundary>
      <p>The rest of the page keeps working.</p>
    </main>
  );
}
What’s happening
  1. On first render, ErrorBoundary's state is { error: null }, so it renders its children: the counter shows "Count: 0" and the paragraph below it renders normally.
  2. Three clicks take count from 0 → 1 → 2 → 3. On the render where count === 3, BuggyCounter throws instead of returning JSX.
  3. React stops rendering that subtree and walks up the tree to the nearest class component with getDerivedStateFromError. It calls it with the error; the returned { error } becomes the boundary's new state, and React re-renders the boundary — now showing the fallback "Something went wrong: I crashed at 3!".
  4. After the fallback is committed to the DOM, React calls componentDidCatch(error, info). That's the place for side effects: here it passes the error and info.componentStack (a string starting with the BuggyCounter frame) to onError, which in a real app would go to your error-reporting service.
  5. The paragraph outside the boundary is untouched — the breaker tripped for one room only. The test checks the alert, the paragraph, and that onError received the error.
  6. "Try again" sets error back to null, so the boundary renders its children again. BuggyCounter was unmounted when it crashed, so it remounts with fresh state (count 0) and works again.

React 19.3 also logs the caught error once with console.error. In the test, the logged arguments are the error object followed by "The above error occurred in the <BuggyCounter> component." and "React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary." Before React 19, a caught error was logged more than once in development (the error itself was re-thrown and reported as uncaught, plus the "above error" message); React 19 reports each error once. What gets logged, and where, can be changed with the root error callbacks.

getDerivedStateFromError vs componentDidCatch

static getDerivedStateFromError(error)componentDidCatch(error, info)
When it runsrender phase, before the fallback is showncommit phase, after the fallback is in the DOM
Jobreturn state that switches to the fallbackside effects: logging, analytics
Has this / side effectsno — static and must be pureyes
Gets the component stacknoyes, info.componentStack

You need getDerivedStateFromError to show a fallback. A class with only componentDidCatch is still treated as a boundary, but React 19.3 warns in development: "OnlyDidCatch: Error boundaries should implement getDerivedStateFromError(). In that method, return a state update to display an error message or fallback UI." The React 16.0 pattern of calling setState in componentDidCatch to switch the UI is legacy.

Error boundaries vs try/catch

From my notes: "Error boundaries are React's way of handling component rendering failures. While try/catch works for imperative code, error boundaries handle failures that occur during the component lifecycle, which is declarative in React." The clearest way to see why you can't just use try/catch:

TryCatchJsx.jsx
function Bomb() {
  throw new Error("render boom");
}

export function Wrapper() {
  try {
    return <Bomb />;
  } catch {
    return <p>caught by try/catch</p>; // never runs
  }
}
What’s happening
  1. React calls Wrapper(). Inside the try, <Bomb /> is evaluated — but JSX doesn't call Bomb. It only creates an element object, { type: Bomb, props: {} }, and nothing throws.
  2. Wrapper returns that object and the try block exits normally. The catch can never run.
  3. Later, React renders the element: it calls Bomb(), which throws "render boom". By then, Wrapper's try is long gone from the call stack.
  4. With no boundary above, the error is uncaught and the root unmounts. The test shows render(<Wrapper />) throwing "render boom".
  5. That's the declarative/imperative difference from my notes: you describe the tree, React renders it later, so error handling for rendering has to be part of the tree too — a component above the failing one.

try/catch is still the right tool for imperative code you call yourself: inside event handlers, inside effects, around await in async functions (see Handling errors in events and async code).

AdvancedResetting with a key

"Try again" above works by clearing the boundary's state. Another common technique is giving the boundary a key tied to whatever might fix the error — the current route or the selected item: <ErrorBoundary key={userId}>. When userId changes, React treats it as a different boundary, unmounts the old one (and its error state) and mounts a fresh one. react-error-boundary's resetKeys prop is the same idea without remounting the boundary itself.

#What error boundaries don't catch

An error boundary only catches errors that happen while React is rendering or committing the tree below it. Anything that runs at another time, called by someone other than React's render, is outside its reach. From my notes, they do not catch errors in:

  • Event handlers (onClick, onSubmit…).
  • Asynchronous code (e.g. setTimeout callbacks, fetch promise callbacks).
  • Server-side rendering.
  • The error boundary itself — "though another error boundary can catch that".

The mental model: a boundary is a try/catch that React wraps around rendering. An event handler runs when the user clicks, long after rendering finished; a timer callback runs from the event loop. Neither is inside React's render, so neither is inside the boundary's try.

NotCaught.jsx
export function ClickBomb() {
  return (
    <button
      onClick={() => {
        throw new Error("click boom");
      }}
    >
      Break on click
    </button>
  );
}

export function TimerBomb() {
  return (
    <button
      onClick={() =>
        setTimeout(() => {
          throw new Error("timer boom");
        }, 0)
      }
    >
      Break later
    </button>
  );
}
What’s happening
  1. Both buttons are rendered inside an ErrorBoundary in the test, and rendering succeeds — nothing throws during render.
  2. Clicking "Break on click" runs the handler, which throws. React calls event handlers itself, catches the error so the rest of the dispatch can finish, and reports it as an uncaught error (window.reportError, which fires the window's error event). The test's error listener receives "click boom".
  3. The boundary's state never changes: no fallback is shown, and the button is still there and clickable. The UI isn't broken — the handler just failed.
  4. Clicking "Break later" schedules a timer. When it fires, the callback throws from the event loop with no React frame anywhere on the stack: a plain uncaught exception. In a browser it goes to the window's error event; in the jsdom test it surfaces to the test runner, so the test runs the timer with fake timers and asserts that it throws "timer boom". Either way, the boundary does nothing.
  5. This is why errors in handlers and async code need their own handling: try/catch, error state, or forwarding the error to a boundary on purpose (next topics).

What they do catch that surprises people

My notes' list is right, but it's worth being precise in the other direction, because several "async-looking" errors are routed to boundaries in React 19:

Caught.jsx
import { useEffect, useTransition } from "react";

export function EffectBomb() {
  useEffect(() => {
    throw new Error("effect boom");
  }, []);
  return <p>mounted</p>;
}

export function TransitionBomb() {
  const [isPending, startTransition] = useTransition();
  return (
    <button
      disabled={isPending}
      onClick={() =>
        startTransition(async () => {
          await Promise.resolve();
          throw new Error("action boom");
        })
      }
    >
      Save
    </button>
  );
}
What’s happening
  1. EffectBomb renders fine, then its effect throws after commit. Effects run as part of React's commit work for that component, so React treats this like a render error: the nearest boundary shows its fallback ("effect boom"). Lifecycle methods (componentDidMount, componentDidUpdate) and constructors behave the same way.
  2. TransitionBomb's click handler doesn't throw itself; it calls startTransition with an async function (a React 19 Action).
  3. The action awaits, then throws. In React 19, an error thrown from a function passed to startTransition is re-thrown during the next render of that component, so the nearest error boundary catches it. The test sees the fallback "action boom".
  4. The same applies to use(promise) when the promise rejects, and to React.lazy when the import fails (as in the RemoteSlot example): the rejection surfaces during render, so a boundary catches it.
  5. So the accurate rule is: if the error surfaces during React's render or commit, a boundary catches it; a raw event handler or a raw callback (setTimeout, a fetch(...).then you didn't connect to React) isn't caught.

Errors in the boundary itself

A boundary can't catch an error thrown by its own render — including its fallback. The error goes to the next boundary up:

NestedBoundaries.jsx
import { ErrorBoundary } from "./ErrorBoundary";

class BrokenFallbackBoundary extends ErrorBoundary {
  render() {
    if (this.state.error) throw new Error("fallback boom"); // a bug in the fallback
    return this.props.children;
  }
}

function Bomb() {
  throw new Error("child boom");
}

export function Nested() {
  return (
    <ErrorBoundary>
      <BrokenFallbackBoundary>
        <Bomb />
      </BrokenFallbackBoundary>
    </ErrorBoundary>
  );
}
What’s happening
  1. Bomb throws "child boom". The nearest boundary is BrokenFallbackBoundary; its inherited getDerivedStateFromError stores the error.
  2. React re-renders BrokenFallbackBoundary to show its fallback — and that render throws "fallback boom". A boundary only protects its children, not itself.
  3. The error continues upward to the outer ErrorBoundary, which shows "Something went wrong: fallback boom". The test checks exactly that text.
  4. The lesson from my notes ("another error boundary can catch that"): keep fallbacks simple and dependency-free, and keep a top-level boundary as a last line of defence.
Which errors does the boundary catch?
React
<ErrorBoundary fallback={<p>Oops</p>}>
  <A /> {/* throws during render */}
  <B /> {/* onClick={() => { throw ... }} */}
  <C /> {/* useEffect(() => { throw ... }, []) */}
  <D /> {/* onClick={() => setTimeout(() => { throw ... })} */}
  <E /> {/* onClick={() => startTransition(async () => { throw ... })} */}
</ErrorBoundary>
Show answer

A, C and E are caught. B and D are not.

What’s happening
  1. A throws during render — the textbook case. Caught.
  2. B throws inside an event handler, which React calls outside rendering. Not caught; it's reported as an uncaught error and the UI stays as it was.
  3. C throws in an effect, which runs as part of React's commit for that component. Caught.
  4. D throws from a timer callback on the event loop, with no React code on the stack. Not caught.
  5. E throws inside a React 19 transition Action; React re-throws it during render. Caught. (In React 18, startTransition took only synchronous functions and this pattern didn't exist.)

#react-error-boundary

Writing the class above in every project gets old, and it's missing features you end up needing: different fallback styles, a reset API, resetting when some value changes, a way to send event-handler errors to the boundary. react-error-boundary (by Brian Vaughn, formerly of the React core team) is that class, done once. My notes: "You can use the popular react-error-boundary library, which provides hooks for using error boundaries in functional components."

A precise version of that sentence: the library still uses a class internally (it has to), but it gives you a ready-made <ErrorBoundary> component, a useErrorBoundary hook to trigger and reset the nearest boundary from function components, and a withErrorBoundary HOC. The installed version is 6.1.6.

From my notes, the full example:

RandomError.jsx
import { ErrorBoundary } from "react-error-boundary";

// Functional Component
function MyComponent() {
  if (Math.random() > 0.7) {
    throw new Error("Random error!");
  }
  return <h1>Hello from MyComponent!</h1>;
}

// Fallback Component
function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

// Usage of ErrorBoundary with a functional component
export function App({ onReset = () => {} }) {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        // Reset the state of the app so the error doesn't happen again
        onReset();
      }}
    >
      <MyComponent />
    </ErrorBoundary>
  );
}
What’s happening
  1. MyComponent throws about 30% of the time (Math.random() > 0.7). The test makes that deterministic by stubbing Math.random to return 0.9 (throw) until the fallback is showing, then 0.1 (render).
  2. When it throws, ErrorBoundary catches it and renders FallbackComponent with two props: error (the thrown value) and resetErrorBoundary. The alert shows "Something went wrong:" and "Random error!".
  3. Clicking "Try again" calls resetErrorBoundary(). The boundary first calls onReset — in my notes, the place to "reset the state of the app so the error doesn't happen again" — then clears its error and renders the children again.
  4. This time Math.random() returns 0.1, so MyComponent renders "Hello from MyComponent!". The test checks the heading and that onReset was called once (with { reason: "imperative-api", args: [...] } describing why).
  5. If the cause isn't fixed, the retry just throws again and the fallback comes back — resetting is only useful when something changed.

Updates for version 6 (correcting my notes for TypeScript)

The JSX above works unchanged in v6. Two things changed that matter, especially in TypeScript:

  • error is typed unknown, not Error (JavaScript lets you throw anything — a string, null, an object). So error.message in a .tsx fallback is a compile error. v6 ships a helper, getErrorMessage(error), that returns the message for Error-like values.
  • v6 is ESM-only. Its README says projects whose framework or runtime doesn't support ES modules should stay on version 5.
Fallbacks.tsx
import { ErrorBoundary, getErrorMessage, type FallbackProps } from "react-error-boundary";

function OldFallback({ error }: FallbackProps) {
  return <pre>{error.message}</pre>; // ❌ 'error' is of type 'unknown'.
}

function Fallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div role="alert">
      <pre>{getErrorMessage(error) ?? "Unknown error"}</pre>
      <button onClick={() => resetErrorBoundary()}>Try again</button>
    </div>
  );
}

function UserCard({ id }: { id: number }) {
  if (id < 0) throw new Error(`No user with id ${id}`);
  return <p>User #{id}</p>;
}

export function Profile({ userId, onError }: { userId: number; onError: (e: unknown) => void }) {
  return (
    <ErrorBoundary FallbackComponent={Fallback} onError={(error) => onError(error)} resetKeys={[userId]}>
      <UserCard id={userId} />
    </ErrorBoundary>
  );
}

export { OldFallback };
What’s happening
  1. OldFallback is my notes' fallback, typed. In v6, FallbackProps["error"] is unknown, so error.message fails with TS18046: "'error' is of type 'unknown'."
  2. Fallback uses getErrorMessage(error), which returns a string | undefined, and falls back to "Unknown error" for things like throw "oops" that have no message.
  3. onError is called once per caught error with (error, info) — the hook for logging (the library calls it from componentDidCatch).
  4. resetKeys={[userId]} is the declarative reset: whenever any value in the array changes, a boundary that is showing its fallback resets and renders its children again. Rendering Profile with userId={-1} shows "No user with id -1"; re-rendering with userId={2} shows "User #2" — no button needed. The test does exactly that.
  5. The three ways to render a fallback are mutually exclusive props (the types enforce it): fallback={<p>…</p>} for static content, FallbackComponent={Fallback} for a component, and fallbackRender={(props) => …} for a render prop.
AdvancedwithErrorBoundary and its log line

withErrorBoundary(Component, { fallback: <p>Widget failed</p>, onError }) returns a wrapped component — handy for wrapping widgets at their export. One small quirk you'll notice in the console: React 19's "React will try to recreate this component tree from scratch using the error boundary you provided, …" ends with the boundary's class name, and in the published (minified) react-error-boundary build that name is a single letter — in this sandbox it printed "…you provided, y." It's still the library's ErrorBoundary; set up root error callbacks if you want your own log format.

#Root error callbacks in React 19

Added

React 19 lets you decide, once per root, what happens to every error React handles. createRoot and hydrateRoot accept three options:

  • onCaughtError(error, info) — an error that an error boundary caught. info has componentStack and errorBoundary (the boundary instance). Default: console.error.
  • onUncaughtError(error, info) — an error that no boundary caught, so React unmounted the whole root. Default: window.reportError.
  • onRecoverableError(error, info) — an error React recovered from by itself, most commonly a hydration mismatch (it threw away the server HTML and rendered on the client). Default: window.reportError. This one existed in React 18; the other two are new in React 19.

The mental model: these are the root's switchboard. Every error ends up on one of three lines, and you decide where each line rings — the console, your error-monitoring service, an analytics event. Before React 19, the only way to see caught errors centrally was to add logging to every boundary's componentDidCatch, and uncaught errors had to be picked up from window.onerror.

errorReporting.tsx
import { createRoot } from "react-dom/client";

export type ErrorKind = "caught" | "uncaught" | "recoverable";
export type Reporter = (kind: ErrorKind, error: unknown, componentStack?: string) => void;

export function createAppRoot(container: Element, report: Reporter) {
  return createRoot(container, {
    onCaughtError(error, info) {
      report("caught", error, info.componentStack);
    },
    onUncaughtError(error, info) {
      report("uncaught", error, info.componentStack);
    },
    onRecoverableError(error, info) {
      report("recoverable", error, info.componentStack);
    },
  });
}

// main.tsx in a real app:
// createAppRoot(document.getElementById("root")!, (kind, error, stack) => monitoring.capture(error, { kind, stack }))
//   .render(<App />);
What’s happening
  1. createAppRoot wraps createRoot and installs all three callbacks. The error parameters are typed unknown (anything can be thrown) and info.componentStack is a string describing where in the tree it happened.
  2. Rendering a tree where a boundary catches a throwing child calls onCaughtError: the test's reporter receives ("caught", Error("render boom"), "\n at Bomb …"), and the info.errorBoundary field (not forwarded here) is the boundary instance. Because the option is set, React does not also log it with console.error — you've replaced the default.
  3. Rendering a tree that throws with no boundary calls onUncaughtError with the error, and React unmounts everything in the root: the test sees the container's HTML become an empty string.
  4. hydrateRoot takes the same options. In the test, hydrating server HTML <p>server</p> with a client tree that renders <p>client</p> calls onRecoverableError with a message starting "Hydration failed because the server rendered text didn't match the client." — and the DOM ends up as <p>client</p>, because React recovered by client-rendering.
  5. In production, the report function would forward to your monitoring service; error-monitoring SDKs such as Sentry's provide ready-made handlers for exactly these three options.
AdvancedcaptureOwnerStack (React 19.1+)

componentStack is the parent chain — which components contain the failing one. For bugs, the owner chain is often more useful: which component rendered (created the JSX for) the failing one. captureOwnerStack() from react (added in React 19.1, dev builds only) returns it as a string, or null in production builds and outside rendering. A dev-only reporter can attach it: onCaughtError(error, info) { report(error, info.componentStack, captureOwnerStack()) }.

#Handling errors in events and async code

Added

Because boundaries don't see errors from event handlers and plain async callbacks, those errors need deliberate handling in the handler itself. There are three good options, and choosing between them is a UX decision:

  • Handle it locally with try/catch and error state, when the user can do something about it — "Couldn't save, try again" next to the button.
  • Forward it to the nearest boundary, when the failure means this whole section can't work. react-error-boundary's showBoundary(error) does that; so does throwing inside a React 19 transition Action.
  • Let it escape to a global handler, for logging only — never as your only handling, because the user sees nothing.

The mental model: an error from a click is a message that arrives after the render has finished. Someone has to receive it — the handler (local state), a boundary (by explicit forwarding) or the window (logging). If nobody does, the user just sees a button that silently didn't work.

SaveButton.jsx
import { useState } from "react";

export function SaveButton({ save }) {
  const [status, setStatus] = useState("idle"); // "idle" | "saving" | "saved"
  const [error, setError] = useState(null);

  async function handleClick() {
    setStatus("saving");
    setError(null);
    try {
      await save();
      setStatus("saved");
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e));
      setStatus("idle");
    }
  }

  return (
    <div>
      <button onClick={handleClick} disabled={status === "saving"}>
        {status === "saving" ? "Saving…" : "Save"}
      </button>
      {status === "saved" && <p>Saved</p>}
      {error && <p role="alert">Couldn't save: {error}</p>}
    </div>
  );
}
What’s happening
  1. Clicking Save sets status to "saving" (the button shows "Saving…" and is disabled, which prevents double submits) and clears any previous error.
  2. await save() runs the request. The try covers the await: a rejected promise becomes a thrown error at that line, so catch receives it. Without the try, the rejection would be an unhandled promise rejection that no boundary sees.
  3. If save rejects with Error("Network down"), catch stores the message and sets status back to "idle": the alert reads "Couldn't save: Network down" and the button is enabled again, so the user can retry.
  4. If it resolves, status becomes "saved" and "Saved" appears. The test runs the failure first, then makes save succeed on the retry.
  5. e instanceof Error ? e.message : String(e) handles the fact that anything can be thrown. In TypeScript, e is unknown in a catch, which forces this check.

Forwarding an error to a boundary

When a failure means the section can't continue — the data the whole panel needs failed to load — show the boundary's fallback instead of inventing a second error UI. Two ways:

Forwarding.jsx
import { useTransition } from "react";
import { ErrorBoundary, useErrorBoundary } from "react-error-boundary";

export function LoadReportButton({ loadReport }) {
  const { showBoundary } = useErrorBoundary();
  return (
    <button
      onClick={() => {
        loadReport().catch((error) => showBoundary(error)); // react-error-boundary
      }}
    >
      Load report
    </button>
  );
}

export function DeleteButton({ remove }) {
  const [isPending, startTransition] = useTransition();
  return (
    <button disabled={isPending} onClick={() => startTransition(async () => await remove())}>
      Delete
    </button>
  );
}

export function Panel({ loadReport, remove }) {
  return (
    <ErrorBoundary fallbackRender={({ error }) => <p role="alert">Panel failed: {error.message}</p>}>
      <LoadReportButton loadReport={loadReport} />
      <DeleteButton remove={remove} />
    </ErrorBoundary>
  );
}
What’s happening
  1. useErrorBoundary() gives LoadReportButton the nearest boundary's showBoundary function (it must be rendered inside an ErrorBoundary subtree).
  2. When loadReport() rejects, the .catch calls showBoundary(error). The library stores the error in state and throws it during the next render, so the boundary catches it like a render error: "Panel failed: Report service unavailable". The test checks it.
  3. DeleteButton uses React 19's built-in route: it runs the async work inside startTransition. If remove() rejects, the Action throws, React re-throws during render, and the same boundary shows "Panel failed: …" — no library hook needed. The button is disabled while isPending.
  4. Both buttons are replaced by the fallback, because the boundary wraps the whole panel. That's the intended UX: this panel is broken, the rest of the page isn't.
  5. To recover, the fallback can call resetErrorBoundary, or use resetKeys as in react-error-boundary.

A global safety net for logging

Some errors will always escape: a third-party script, a forgotten .catch. Two window events catch everything that bubbles out, which is ideal for logging:

globalHandlers.js
export function installGlobalErrorLogging(report) {
  const onError = (event) => report("error", event.error ?? event.message);
  const onRejection = (event) => report("unhandledrejection", event.reason);
  window.addEventListener("error", onError);
  window.addEventListener("unhandledrejection", onRejection);
  return () => {
    window.removeEventListener("error", onError);
    window.removeEventListener("unhandledrejection", onRejection);
  };
}
What’s happening
  1. The error event fires for uncaught exceptions: a throwing event handler, a throwing setTimeout callback, and React's own reportError calls for uncaught render errors (the default onUncaughtError). event.error is the thrown value.
  2. The unhandledrejection event fires when a promise rejects and nothing handles it by the end of the microtask checkpoint — the classic fetch(url).then(...) with no .catch. event.reason is the rejection value.
  3. installGlobalErrorLogging returns an uninstall function, which keeps tests clean and lets you call it from a useEffect if you prefer.
  4. In the test, dispatching an ErrorEvent with error: new Error("third-party boom") reaches the reporter as ("error", Error("third-party boom")), and clicking the ClickBomb button from What error boundaries don't catch does too.
  5. This is a net, not handling: it can't show the user anything sensible. Pair it with local handling and boundaries.

Built from Rohit’s “React Interview concepts” doc. Examples target React 19.3 and are tested with Vitest and React Testing Library.

RohitDownloads · All chapters

Sync progress across devices

Type the same private phrase on your Mac and your phone, and your Learned ticks follow you between them — on every study site.

The phrase never leaves this device: only a fingerprint of it is sent, and the server stores a fingerprint of that. Anyone who knows the phrase could see or change your ticks, so pick something you don’t use elsewhere.

Sync is on in this browser.

Sync ID

This ID must be the same on every device. If another device shows a different one, its phrase is different (capital letters count): tap “Turn off here” on it and type the phrase again exactly.