Component request: `ErrorBoundary`
- Dominant language
- TypeScript
- Stars
- 3.9k
- Forks
- 177
- Avg merge
- 1d 5h
- Merged PRs (30d)
- 48
Description
## 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 `
`, 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
```tsx
import { ErrorBoundary } from "@cloudflare/kumo";// Default fallback UI (warning icon + title + error message + Try again button)
// Custom fallback via render prop
(
{error.message}
Retry
)}
>
```
### 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:
```ts
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:
```ts
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.
Contributor guide
Assessment
This issue has not been assessed yet.