Skip to content

Component request: ErrorBoundary #561

Description

@awhobbs87

Overview

Request to add an ErrorBoundary component to Kumo. The CONTRIBUTING.md guidance suggests opening an issue first for non-trivial additions, so this is a proposal: please push back on shape / scope / whether this belongs in Kumo at all before any code lands.

Motivation

Every app that consumes Kumo ends up writing its own error boundary, because React doesn't ship one and Kumo doesn't either. They tend to look very similar:

  • A class component (still the only way to do getDerivedStateFromError / componentDidCatch).
  • A default fallback UI with a warning icon, a title, the error message in a <pre>, and a "Try again" button.
  • A name prop so the caller can label which boundary tripped, both for the fallback heading and for the console log.
  • Optional render-prop fallback for callers who want their own UI: fallback?: (error, reset) => ReactNode.

The bit that differs in production-grade implementations is error classification + a smarter reset behaviour:

  • Hook-order violations (Rendered fewer hooks than expected, Rules of Hooks) - log a specific Rules-of-Hooks pointer in addition to the regular stack, because retrying re-renders the same broken code path. These are always bugs, not transient failures.
  • Chunk-loading failures (Failed to fetch dynamically imported module, Loading chunk N failed, Vite-HMR MIME mismatch errors) - the broken module will keep throwing on retry, so "Try again" should force a full page reload (window.location.reload()) instead of clearing the error state.
  • Generic render errors - the default "Try again" that clears state and re-renders the subtree.

I have a working implementation of all of the above in an internal Cloudflare app. It's ~170 lines including the fallback UI and error-classifier regexes. Happy to port it to Kumo's conventions (compound component pattern if appropriate, semantic color tokens instead of raw Tailwind, cn(), displayName, JSDoc, KUMO_*_VARIANTS, etc.).

Proposed API

import { ErrorBoundary } from "@cloudflare/kumo";

// Default fallback UI (warning icon + title + error message + Try again button)
<ErrorBoundary name="Sidebar">
  <AppSidebar />
</ErrorBoundary>

// Custom fallback via render prop
<ErrorBoundary
  name="Dashboard"
  fallback={(error, reset) => (
    <div>
      <p>{error.message}</p>
      <Button onClick={reset}>Retry</Button>
    </div>
  )}
>
  <Dashboard />
</ErrorBoundary>

Props

  • children: ReactNode - required, the subtree to wrap.
  • name?: string - optional label, shown in the fallback heading (Something went wrong in {name}) and prefixed to console logs. Helps diagnose which boundary tripped when several are nested.
  • fallback?: (error: Error, reset: () => void) => ReactNode - optional render prop. When provided, fully replaces the default fallback UI.
  • onError?: (error: Error, info: ErrorInfo) => void - optional callback for telemetry pipelines (Sentry, Datadog, etc.). Fires from componentDidCatch.

Behaviour

  • Default fallback uses role="alert" so screen readers announce errors when they appear.
  • Error classification runs against error.message to bucket into hooks | chunk | generic. The bucket is used to:
    • Tailor the fallback title and message (hook violations get a Rules-of-Hooks pointer).
    • Decide whether reset() clears state (generic / hooks) or reloads the page (chunk).
  • Console logging is always-on, even in production (errors caught by boundaries are real bugs, not noise; suppressing them in prod hurts debugging when you're staring at a Sentry alert and want the stack trace in the user's DevTools).

Why this fits Kumo

  • It's a primitive that every consuming app needs.
  • The default fallback styling (border, padding, typography, button) should match Kumo's design language - which means the right place for it is in Kumo, not duplicated by every consumer.
  • It's framework-agnostic (no React Router / Next.js dependency), so it lives well inside @cloudflare/kumo.

Why this might NOT belong in Kumo

A few reasons to push back, and how I'd respond:

  1. "It's not a UI primitive, it's app infrastructure." The render-prop API + onError + name shape is infrastructure-flavoured, fair. But the default fallback's visual identity is purely Kumo's job, and bundling the two avoids each consumer reinventing the styling.
  2. "react-error-boundary already exists." It does, and it's good. But (a) it doesn't ship classification, and (b) it doesn't ship a Kumo-styled fallback. We could depend on it under the hood and provide the styled fallback + classifier on top, which would shrink the maintenance surface inside Kumo. Happy to take that direction if maintainers prefer.
  3. "Putting Kumo's design language on an error fallback is a footgun for prod." Real concern. Counter: the alternative is each consumer building their own, which is what causes the inconsistency this issue is trying to fix.

What I'd like from this issue

  1. Yes / no on the principle: should this live in Kumo at all? If no, close and I'll keep the implementation app-side.
  2. API shape feedback: are the four props the right surface? Should onError be there from day one or added later? Should fallback get errorKind (the classification result) as a third arg so custom fallbacks can also branch on it?
  3. Implementation direction: roll our own ~170-line class component, or depend on react-error-boundary and layer the classifier + styled fallback on top?

Once the shape is settled, I'll open a PR.

Reference implementation

From the internal reference implementation. The classification regexes:

const HOOKS_PATTERN =
  /rendered (more|fewer) hooks|change in the order of hooks|hooks can only be called/i;
const CHUNK_PATTERN =
  /requested module.*MIME|failed to fetch dynamically imported|loading chunk|dynamically imported module/i;

Reset behaviour:

reset = () => {
  if (this.state.errorKind === "chunk") {
    window.location.reload();
    return;
  }
  this.setState({ hasError: false, error: null, errorKind: null });
};

I'd port the regex-driven classifier as-is, and we can refine the patterns over time as new error shapes show up. The classification doesn't need to be perfect - the buckets are about reset semantics (retry vs reload), and "generic" is a fine fallback when nothing matches.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions