Back in June, I wrote that @boundary was coming and that we shouldn't wait for it in production just yet. Here we are again: it landed in Angular 22.2, and the shipped version does quite a bit more than the one-line teaser we saw at the time.
Let's be honest about the problem it solves. One component throws during rendering, and the whole view goes with it. Not the widget, the page. Until now, the only defence was a global ErrorHandler that could log the crash but couldn't put anything back on screen.
β οΈ Availability:
@boundaryis in developer preview since Angular 22.2 (it first appeared in22.2.0-next.6). Syntax and behaviour can still change before it goes stable.π§ͺ Try it: every snippet in this article runs in the companion demo on GitHub, with an on-screen log of where each error ends up.
π‘οΈ The Basics
The shape is the one from June: a @boundary block wraps the risky part of the template, an @error block says what to show instead.
@boundary {
<app-revenue-chart />
} @error {
<p>The chart couldn't load: {{ $error.message }}</p>
<button (click)="$reset()">Try again</button>
}
Two implicit variables are available inside @error:
-
$erroris the caught error. Anything thrown that isn't anErrorgets wrapped in anErrorBoundaryWrappedError, so$error.messageis always safe to read. -
$resetis a function. Calling$reset()throws away the fallback and tries to render the primary block again, which is your retry button for free.
The let syntax from the June teaser still works, as an alias: @error (let err) makes err point to $error. Those two names are the only ones allowed, so there's no room for typos.
π― Multiple @error Blocks and when
This is the part the teaser didn't show. You can chain several @error blocks, each with a when condition, and the first one whose condition is truthy wins:
@boundary {
<app-chart-dashboard />
} @error (let err; retry = $reset; when isChartError(err)) {
<p>Chart failed: {{ err.message }}</p>
<button (click)="retry()">Retry</button>
} @error {
<p>Something unexpected happened.</p>
}
Nothing better than reading the rules straight from the compiler:
-
whentakes any template expression, it's a predicate rather than a type filter. Usually a component method likeisChartError(err). -
At most one
@errorcan be unconditional, and it has to be the last one in the chain. -
If no block matches, Angular throws a
BoundaryErrorwith the original error as itscause, and that error travels up to the next boundary.
π‘ Tip: the official error-boundaries guide currently pairs an
isRenderError(err)condition with a "Network issue" message. Don't copy that example verbatim.
π§ What a Boundary Catches, and What It Doesn't
The boundary catches errors thrown while Angular creates or updates the views inside it: component constructors, template bindings, lifecycle hooks, @for expressions, effects declared inside the boundary, and lazily or dynamically created components.
What it doesn't catch is just as important:
| Not caught | Where the error goes instead |
|---|---|
Event handlers, e.g. a (click) handler |
The global ErrorHandler. The boundary protects rendering, not interaction |
The @error block itself |
The next boundary up |
Projected content, when the receiving component wraps its <ng-content> in a boundary |
Not caught by that boundary (the official guide lists it as unsupported) |
| Async work that settles outside Angular's render pass, such as a rejected promise you never surface into a signal | Not to the boundary, which only sees errors thrown while rendering |
Nesting follows the same logic. When something throws, Angular walks up the view tree to the closest boundary. If that boundary's handlers don't match or throw themselves, it keeps walking.
When a boundary does catch something, it doesn't swallow it silently. Angular calls ErrorHandler.onViewError(error, details) if you've implemented it, and handleError otherwise. The details object tells you which declaration failed and which boundary caught it, which is exactly what your error tracking wants.
β οΈ Gotcha:
onViewErrorruns synchronously while Angular is rendering. Write to a signal from it (an on-screen error list, a store) and you getNG0600, which breaks the boundary that was trying to report the error. Defer the write, for example withqueueMicrotask(), or send the error straight to your monitoring.
π Where It Gets Interesting: Resources
Here's the detail that makes @boundary click with the rest of modern Angular. Calling value() on a resource() that is in an error state doesn't return undefined. It throws. And a throwing template binding is exactly what a boundary intercepts.
So you don't need an @if around error() anymore: let the template read the value and declare the failure path once. You still need one small guard, though, and it's easy to miss:
@boundary {
@if (order.isLoading() && !order.hasValue()) {
<p>Loading orderβ¦</p>
} @else {
<app-order-summary [order]="order.value()!" />
}
} @error {
<p>We couldn't load this order.</p>
<button (click)="order.reload(); $reset()">Retry</button>
}
The guard is there because value() throws only in the error state. While the resource is loading for the first time, or reloading after an error, it returns undefined, and the child component crashes on that instead. The boundary catches that crash too, so without the guard two things go wrong:
-
The first load lands in the fallback, and it stays there even after the data arrives, because a boundary showing its fallback doesn't re-render the primary block until
$reset(). -
order.reload(); $reset()resets too early. The primary block re-renders while the resource is still reloading, hitsundefined, and the fallback comes straight back.
With the guard, the one-liner retry works: $reset() shows the loading state, then the data. If you'd rather not guard, reload, wait for isLoading() to turn false, and only then call $reset(). The demo has a checkbox that toggles the guard, so you can watch both behaviours.
This fits the Router Resources that shipped in the same release. A nonBlocking() route resource lets navigation finish even if its loader fails, and the failure surfaces through the resource. Put the part of the page that reads it inside a @boundary, and a failed side panel becomes a contained fallback instead of a broken page.
π§ͺ Try It Yourself
The demo on GitHub has every example from this article in one template, in app.ts. Run it, toggle the failures, use the retry buttons, and watch the log at the bottom to see which errors reach onViewError and which go to handleError.
git clone https://github.com/giorgiogalassi/angular-article-demos.git
cd angular-article-demos/demos/2026-10-01-angular-boundary-in-practice
npm ci && npm start
π§ The Programmatic Version
If you render components from code, you get the same mechanism as an onError option on ViewContainerRef.createComponent, createEmbeddedView and createComponent. One difference: unlike @boundary, it doesn't catch errors thrown in the component's constructor.
β How I'd Start Using It
Developer preview means "try it", not "wrap everything". A few sensible first steps:
- Wrap the widgets you don't control: third-party embeds, user-generated content, feature-flagged experiments.
-
Pair it with resources where a missing piece of data shouldn't take down the page, with an
isLoading() && !hasValue()guard inside. -
Implement
onViewErrorso caught errors still reach your monitoring, and keep it free of synchronous signal writes. - Keep event-handler errors in mind, because they're the ones a boundary will never see.
In June, @boundary was a promise of "crash isolation" in the template. In 22.2 it's a full error-handling model, with conditions, retries and a clean hand-off to your ErrorHandler. The template layer keeps getting more resilient, one block at a time.
If you want to go deeper on the template features that got us here, check out my previous articles:
- Angular v22 β
@boundary, Multi-Case Switch, and Inline Arrow Functions - Understanding
@deferβ Part 1 (v18+) - Angular v19+ β Understanding the New
resource()andrxResource()APIs
If you found this helpful, follow me here and on LinkedIn for more deep dives into Angular, signals and modern frontend development.
I also send the longer, more opinionated version of this kind of thing to a small list β reading the source, and what it changes about how you build. Subscribe here.
See you in the next one! π€π»
β G.
Top comments (0)