import { Component, type ErrorInfo, type ReactNode } from "react"; import i18n from "../i18n"; import { notify } from "../lib/notifications"; import { errorAttrs, log } from "../lib/log"; import { reloadingForNewBuild } from "../lib/staleBuild"; import { Button } from "./ui/Button"; interface Props { children: ReactNode; } interface State { crashed: boolean; } // Catches render-time crashes anywhere in the tree, logs a fatal notification (kept // in the center across the reload) and offers a recovery action. export default class ErrorBoundary extends Component { state: State = { crashed: false }; static getDerivedStateFromError(): State { return { crashed: true }; } componentDidCatch(error: Error, info: ErrorInfo) { // The tab is already reloading onto a newer build (lib/staleBuild): this is the chunk it could not // load, not a crash — no fatal notice, which would outlive the reload. if (reloadingForNewBuild()) return; log.fatal("ui.crash", "the app crashed while rendering", { ...errorAttrs(error), ...(info.componentStack ? { component_stack: info.componentStack } : {}), }); notify({ level: "fatal", title: i18n.t("errors.boundary.notifTitle"), // Never the raw `error.message`: a JS engine's English ("Cannot read properties of undefined…") // is no message for the reader (E30) — the `ui.crash` log above carries it for diagnosis. message: i18n.t("errors.boundary.notifMessage"), requiresInteraction: true, }); } render() { if (this.state.crashed) { if (reloadingForNewBuild()) return null; return (
{i18n.t("errors.boundary.title")}
{i18n.t("errors.boundary.subtitle")}
); } return this.props.children; } }