Angular 22.2: Error Boundaries, Router Resources, and More

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.

NOTE

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.

Modern Angular - Signal-first, Architecture-first, Practice-first

More about the book →

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 a RedirectCommand instead of returning it. This makes redirects easier from deeply nested calls, for instance from a catch block.
  • Auto cleanup for route injectors is stable: withExperimentalAutoCleanupInjectors becomes withAutoCleanupInjectors. The router uses it to clean up the environment injectors of routes that are no longer active. Custom RouteReuseStrategy implementations can take part in the decision via the new optional methods retrieveStoredRouteHandles and shouldDestroyInjector.
  • containsTree is public: The function checks whether one UrlTree is contained in another and accepts the same options as isActive. 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 with isolatedDeclarations easier, because private members don't need explicit type annotations.
  • strictUnclaimedEventNames: The new option in angularCompilerOptions reports 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 a DirectiveFixture with access to the directive instance and the host element. Inputs, outputs, and the tag name of the host are configurable.
  • Injector via 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 declareExperimentalWebMcpTool or provideExperimentalWebMcpTools now describe their behavior via annotations with the hints readOnlyHint, consequentialHint, and untrustedContentHint. Signal Forms exposed as tools via the experimentalWebMcpTool option get readOnlyHint and untrustedContentHint fixed to false, since a form by definition changes the state of the page.
  • Signal Forms: hidden without a condition: hidden(path) now hides a field permanently even without a when condition, in line with readonly and disabled.
  • Animations: animate.enter and animate.leave now also accept functions and Signals as bindings.
  • CLI: The CLI's MCP server gets a --root option that restricts access to a subtree in monorepos. --stats-json generates separate files for browser and server in SSR applications. The unit test integration moves to Vitest 5, which is why the splitting option is deprecated. The Angular linker is now based on oxc-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!

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.

Modern Angular

Architecture · Concepts · Implementation

Ein praktischer Leitfaden für skalierbare Angular-Anwendungen mit modernen Architekturmustern und Signals.

Mehr zum Buch