A dashboard with three widgets. The sales widget reads data.points.length, and one day the backend returns { points: null }. On render, null.length throws a TypeError.
The question is what the user sees: the whole screen broken, or just that widget with an error message. Each framework answers differently, and the answer says a lot about how each one is designed.
React: a class component
If a component throws while rendering and nobody catches it, React removes the whole UI from the screen. To prevent that, there's the Error Boundary: a component that implements getDerivedStateFromError to show a fallback and componentDidCatch to log the error.
import { Component } from 'react';
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
logError(error, info.componentStack);
}
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}
<ErrorBoundary fallback={<p>We couldn't load sales.</p>}>
<Sales data={sales} />
</ErrorBoundary>
Two details that surprise people coming from hooks:
-
It has to be a class. The React docs say it plainly: there's currently no way to write an Error Boundary as a function component. The usual way out is the
react-error-boundarylibrary. -
It doesn't catch everything. Event handlers, async code (
setTimeout), server-side rendering and errors thrown by the boundary itself are left out. The exception is thestartTransitionreturned byuseTransition: errors thrown there do reach the boundary (the standalonestartTransitionimported fromreactdoesn't: it goes toreportError).
Why it works this way: in React, error handling lives in the component lifecycle. Hooks have no equivalent of getDerivedStateFromError, so the only piece that can catch its children's errors is a class component.
Next.js: one file per route segment
Next.js builds its boundaries on top of folder-based routing. An error.tsx inside a segment wraps that segment's page and its children in a boundary; it doesn't cover the layout.tsx of the same segment, which is handled by the parent's error.tsx (Next.js 16.3 docs):
'use client'; // error boundaries must be Client Components
export default function ErrorPage({
error,
retry,
}: {
error: Error & { digest?: string };
retry: () => void;
}) {
return (
<div>
<h2>Something went wrong</h2>
<button onClick={() => retry()}>Try again</button>
</div>
);
}
Errors bubble up to the nearest error.tsx, and global-error.tsx covers the root layout. To contain errors at the component level, not just the route level, Next.js has catchError (stable since 16.3), which turns a function into a boundary without writing the class.
Why it works this way: in Next.js, the structure of the app is its folders. It makes sense for the route segment to be the natural unit of error.
Angular before 22.2: only a global handler
Angular only had ErrorHandler.handleError(): a central place to find out about an error and report it, but no built-in way to replace the broken part with a local fallback. As Ninja Squad describes it, if a component threw while rendering, the rest of its subtree wasn't rendered and the page ended up partially blank.
Angular 22.2: @boundary
Angular 22.2, released on September 23, 2026, adds @boundary in developer preview (official guide):
<h1>Dashboard</h1>
@boundary {
<app-sales [data]="sales()" />
} @error (let err; when isNetworkError(err)) {
<p>Network problem.</p>
<button (click)="$reset()">Retry</button>
} @error {
<p>We couldn't load sales: {{ $error.message }}</p>
}
<app-users />
<app-alerts />
Only the sales box shows the fallback; users and alerts render normally. The @error blocks are evaluated in order, and if none matches and there's no @error without when at the end, the error keeps going, wrapped in a BoundaryError with the original in .cause.
According to the framework's tests, the block catches errors thrown by the constructors of the components inside it, their lifecycle hooks, template bindings and their effect()s.
It's not just a template block: three pieces ship together.
| Piece | What it's for |
|---|---|
@boundary / @error
|
The declarative version, in the template |
onError in createComponent and createEmbeddedView
|
The equivalent for views created in code: you get the error in a callback and build the fallback yourself. It doesn't cover the creation phase (the constructor, or creating the view in createEmbeddedView): that throws synchronously from the same call |
ErrorHandler.onViewError() |
Optional hook that receives every error a boundary intercepts, so you can send it to monitoring. If you implement it, those errors no longer go through handleError(); if you don't, they go there |
viewContainerRef.createComponent(Widget, {
onError: (error, details) => report(error),
});
Why it works this way: in Angular, the template compiler already owns control flow (@if, @for, @defer), and a boundary is one more block, not a component. The programmatic API completes the picture: a component you insert into a ViewContainerRef located inside a @boundary is already covered (a framework test checks exactly that), but not every dynamic view has a block around it, and that's what onError is for.
Where to put them: Angular's guide doesn't say, but react.dev's criterion applies just as well. You don't need to wrap every component: a boundary goes where it makes sense to show an error message, like a widget or a third-party component. And errors you can anticipate, like a failing fetch, Angular recommends handling where they happen, with try/catch. The ErrorHandler is for reporting the unexpected.
Why we think Angular solves it better
This section is opinion:
-
You don't need a class or a library. It's template syntax, just like
@if. -
You discriminate by error type in the template. Several
@error (when ...)blocks with a catch-all at the end. In React, that logic goes inside the fallback. - Declarative, programmatic and global are part of the same feature. No need to assemble the puzzle from loose pieces.
To be fair:
-
@boundaryis in developer preview: per the versioning policy, it can change even in a patch release. React's boundaries have years in production. - Next.js's
catchErroralready solves the "I don't want to write a class" problem. And React also has a global callback:createRootacceptsonCaughtError, which receives the errors a boundary catches. -
It doesn't catch event handlers either. In Angular 22.2's code, an error inside a listener goes straight to the global
ErrorHandler, without going through any boundary (listeners.ts). Same limit as React. -
Watch out for content projection. Wrapping
<ng-content>in a@boundarydoesn't catch errors from projected content: that content belongs to the view that declares it. You have to wrap the container component from the parent template (guide). In React, a boundary that wrapschildrendoes catch their errors.
Summary
-
React: a class component, or
react-error-boundary. -
Next.js:
error.tsxper route andcatchErrorper component. - Angular 22.2: a template block, with a programmatic equivalent and a global hook.
Three different designs for the same problem, each one stemming from how its framework is built.



Top comments (0)