DEV Community

Cover image for Template errors: React and Next.js vs Angular 22.2's @boundary
Damian Sire (Dev)
Damian Sire (Dev)

Posted on

Template errors: React and Next.js vs Angular 22.2's @boundary

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.

Same bug, two outcomes: without a boundary, React removes the whole UI and only a TypeError is left in the console; with a boundary around the sales widget, only that widget shows its fallback while users and alerts keep working.

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>
Enter fullscreen mode Exit fullscreen mode

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-boundary library.
  • 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 the startTransition returned by useTransition: errors thrown there do reach the boundary (the standalone startTransition imported from react doesn't: it goes to reportError).

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Next.js App Router nesting: errors in app/dashboard/page.tsx are caught by app/dashboard/error.tsx, errors in app/dashboard/layout.tsx by the parent app/error.tsx, and errors in the root app/layout.tsx by global-error.tsx.

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 />
Enter fullscreen mode Exit fullscreen mode

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.

Angular 22.2 @boundary flow: an error inside the block (constructor, lifecycle hook, template binding or effect) first notifies the global ErrorHandler, through onViewError or else handleError; then the @error blocks are checked in order. The first match renders its fallback; if none matches, 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),
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. You don't need a class or a library. It's template syntax, just like @if.
  2. 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.
  3. Declarative, programmatic and global are part of the same feature. No need to assemble the puzzle from loose pieces.

To be fair:

  • @boundary is 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 catchError already solves the "I don't want to write a class" problem. And React also has a global callback: createRoot accepts onCaughtError, 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 @boundary doesn'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 wraps children does catch their errors.

Summary

  • React: a class component, or react-error-boundary.
  • Next.js: error.tsx per route and catchError per 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)