cloudflare / cloudflare/kumo

Component request: `ErrorBoundary`

Open
#561 2 comments 1 reaction 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.