Angular 22.2 was released at the end of September 2026. Two additions stand out, both as developer previews: Error boundaries catch errors while rendering a section of a template and show a fallback view instead. Router Resources load route data with resources instead of resolvers, in parallel rather than one after another. This article shows both using our demo application Flights42 and then summarizes further interesting changes from the changelog.
📂 Source code (branch: 22.2)
Error Boundaries with @boundary
When a template threw an exception while rendering, there was no elegant way to limit its impact to one part of the page. The error ended up in the global ErrorHandler, and the affected view remained incomplete. The new @boundary block lets you protect any section of a template: if an error occurs inside it during the initial render or a later change detection, Angular stops rendering that section and shows the associated @error block instead. The concept is inspired by error boundaries in React.
Resources are a typical use case. If a resource's loader fails, its value() throws the error as an exception. This behavior is intentional: if value() silently returned an empty value in the error case, the error could easily go unnoticed. Until now, a template therefore had to check the error() signal before accessing value(). For the luggage detail page of the demo, that looked like this:
@let resource = luggage();
@if (resource.isLoading()) {
<p>Loading luggage …</p>
} @else if (resource.error()) {
<p>Luggage could not be loaded.</p>
} @else {
@let item = resource.value();
<form> [...] </form>
}
With Angular 22.2, a @boundary block takes over this job:
<!-- [...]/luggage/feature-luggage/luggage-detail/luggage-detail.html -->
@boundary {
@let resource = luggage();
@let item = resource.value();
@if (resource.isLoading()) {
<p>Loading luggage …</p>
} @else {
<form> [...] </form>
}
} @error {
<p>Luggage could not be loaded.</p>
}
<p class="mt-40">
<a routerLink="/luggage">Back to overview</a>
</p>
The component receives the resource as an input from the router. More on that in a moment. The template now simply reads the value without checking the error state first. If value() throws, the boundary catches the exception and renders the @error block. The link back to the overview is outside the boundary and therefore stays visible in any case. Navigating to the detail page in the demo with an invalid id, e.g. /luggage/9999, makes the loader throw an error, and the page shows the message from the @error block.
A boundary can span any part of the template and also protects the components used within it: if a child component throws while rendering or during change detection, the error also ends up in the closest enclosing boundary. Boundaries can be nested. Two points are worth noting: the boundary doesn't catch errors in projected content, that is, in <ng-content>, and errors inside the @error block itself propagate to the next outer boundary.
Handling Caught Errors
The @error block provides the error via the implicit variable $error, similar to $index in @for. It can be renamed with let, and when allows several blocks for different kinds of errors. For the optimists, there is also the implicit function $reset, which simply tries rendering once more:
@boundary {
<app-chart />
} @error (let err; retry = $reset; when isNetworkError(err)) {
<p>{{ err.message }}</p>
<button (click)="retry()">Retry</button>
} @error {
<p>Something went wrong.</p>
}
A caught error doesn't silently disappear into the fallback view. Angular additionally reports it to the ErrorHandler, namely to its new optional method onViewError if implemented, otherwise to handleError. Without a custom ErrorHandler, the error therefore still shows up in the console.
Errors can also be caught when creating components and views dynamically: createComponent and the corresponding methods of ViewContainerRef accept an onError callback. Besides the error, both onViewError and onError receive an ErrorDetails object that names the affected component and, if present, the catching boundary along with its reset function. Error boundaries ship in 22.2 as a developer preview.
Modern Angular
✓ Already updated to Angular 22!
You'll find more on resources, Signals, and modern Angular architecture in my new eBook Modern Angular. It covers Signals, architecture, testing, AI assistants, and practical solutions for modern business applications.
Router Resources
Resolvers load data before a route is activated and return an Observable or Promise for it. In Angular's Signal world, that felt out of place. Router Resources now solve this and also bring a performance benefit: the router starts all resources of a navigation at the same time, whereas it processes resolvers one after another along the route hierarchy. If, for example, the parent route needs 2 seconds for its data and the child route 3, the user waits 5 seconds for the navigation with resolvers. With Router Resources, it takes only 3 seconds, as long as the slowest resource.
The feature is enabled via withRouterResources:
// src/app/app.config.ts
import {
provideRouter,
withComponentInputBinding,
withRouterResources,
} from '@angular/router';
[...]
provideRouter(routes, withComponentInputBinding(), withRouterResources()),
A route declares its data in the resources property. The function runs in an injection context and receives a ResourceContext with params, queryParams, fragment, and data as Signals. The route for editing a passenger loads the record before the component appears:
// src/app/domains/ticketing/ticketing.routes.ts
{
path: 'passenger-edit/:id',
component: PassengerEdit,
resources: (ctx) => ({
passenger: createSimplePassengerResource(ctx.params),
}),
},
The factory function derives the id from the parameters and creates an ordinary httpResource:
// [...]/feature-booking/passenger-edit/simple-passenger-resource.ts
export function createSimplePassengerResource(params: Signal<Params>) {
const passengerClient = inject(PassengerClient);
const id = computed(() => Number(params()['id'] ?? 0));
return passengerClient.findPassengerResourceById(id);
}
Such a resource is blocking: like with a resolver, the router waits until the value is available. Thanks to withComponentInputBinding, the component receives the unwrapped value as an input, exactly as it did from the resolver before. In the prereleases of 22.2, a resource with a default value didn't block, because from the router's point of view a value was already present. The final release looks solely at the loading state. A default value is therefore no longer an obstacle.
Non-Blocking Router Resources
Navigation shouldn't always wait. With nonBlocking, the router activates the route immediately and hands the still-loading resource as a whole to the component. The luggage detail page from the first section uses exactly that:
// src/app/domains/luggage/feature-luggage/luggage.routes.ts
import { nonBlocking, Routes } from '@angular/router';
[...]
{
path: ':id',
component: LuggageDetail,
resources: (ctx) => ({
luggage: nonBlocking(createLuggageResource(ctx.params)),
}),
},
// [...]/feature-luggage/luggage-detail/luggage-detail.ts
@Component({ [...] })
export class LuggageDetail {
readonly luggage = input.required<Resource<Luggage>>();
}
This makes isLoading, error, and value available in the template, and the error boundary from the first section takes care of the error case.
Router Resources can do more: via the ActivatedRoute, a resource can be reloaded without a new navigation, and a RedirectCommand thrown in the loader redirects to another route when data is missing. These details, as well as the interplay with Signal Stores, are covered in the article Router Resources: Loading Data with the Angular Router.
More at a Glance
Besides the two big new features, error boundaries and Router Resources, Angular 22.2 comes with numerous refinements. They touch the router, the compiler, testing, Signal Forms, and the CLI. The most interesting ones at a glance:
-
Throwing
RedirectCommand: Guards, resolvers, and Router Resources can now throw aRedirectCommandinstead of returning it. This makes redirects easier from deeply nested calls, for instance from acatchblock. -
Auto cleanup for route injectors is stable:
withExperimentalAutoCleanupInjectorsbecomeswithAutoCleanupInjectors. The router uses it to clean up the environment injectors of routes that are no longer active. CustomRouteReuseStrategyimplementations can take part in the decision via the new optional methodsretrieveStoredRouteHandlesandshouldDestroyInjector. -
containsTreeis public: The function checks whether oneUrlTreeis contained in another and accepts the same options asisActive. Until now, it was only available internally. -
Private members in templates: Templates may now access properties and methods of the component declared as
private. This makes working withisolatedDeclarationseasier, because private members don't need explicit type annotations. -
strictUnclaimedEventNames: The new option inangularCompilerOptionsreports event bindings that are neither an output of an applied directive nor a known DOM event, for instance(userSelcted)instead of(userSelected). It is opt-in and applies to camelCase names. -
TestBed.createDirective: Directives can be tested without a hand-written host component. The method returns aDirectiveFixturewith access to the directive instance and the host element. Inputs, outputs, and the tag name of the host are configurable. -
Injectorvia query: View and content queries can read the injector of the matched node with{ read: Injector }. This comes in handy when dynamically created content needs the injector of a specific place in the template. -
WebMCP annotations: Tools that an application provides for AI agents via
declareExperimentalWebMcpToolorprovideExperimentalWebMcpToolsnow describe their behavior viaannotationswith the hintsreadOnlyHint,consequentialHint, anduntrustedContentHint. Signal Forms exposed as tools via theexperimentalWebMcpTooloption getreadOnlyHintanduntrustedContentHintfixed tofalse, since a form by definition changes the state of the page. -
Signal Forms:
hiddenwithout a condition:hidden(path)now hides a field permanently even without awhencondition, in line withreadonlyanddisabled. -
Animations:
animate.enterandanimate.leavenow also accept functions and Signals as bindings. -
CLI: The CLI's MCP server gets a
--rootoption that restricts access to a subtree in monorepos.--stats-jsongenerates separate files for browser and server in SSR applications. The unit test integration moves to Vitest 5, which is why thesplittingoption is deprecated. The Angular linker is now based onoxc-parser, and numerous performance improvements speed up builds, including decoupling type checking from the build start, a native Sass daemon, and a new file watcher.
Learn More: Angular Architecture Workshop (Remote, Interactive, Advanced)
Become an expert in enterprise-wide and long-lived Angular applications with our Angular Architecture Workshop!

German Version | English Version
Conclusion
Angular 22.2 is a minor release with two additions that noticeably change everyday work. Error boundaries give templates a mechanism other frameworks have known for a while, and they go well with the Resource API, whose value() throws in the error case. Router Resources bring the loading of route data into the Signal world and get rid of the resolver waterfall. Both are marked as developer previews, but the direction is set. On top of that come smaller improvements such as throwing RedirectCommand, TestBed.createDirective, and a build that gets faster thanks to decoupled type checking.
