Angular 22.2 arrived at the end of September 2026. Two additions stand out, both shipped as developer previews: Error boundaries trap errors thrown while a section of a template renders and display a fallback instead. Router Resources replace resolvers for loading route data, and they fetch in parallel rather than sequentially. Below, both features are demonstrated with our demo application Flights42, followed by a summary of other notable changelog entries.

📂 Source code (branch: 22.2)

Catching Render Errors with @boundary

Previously, when a template threw during rendering, there was no clean way to confine the damage to a single region of the page. The exception landed in the global ErrorHandler, and the affected view stayed partially rendered. The new @boundary block lets you wrap any section of a template: should an error occur inside it — whether on the initial render or during a later change detection cycle — Angular halts rendering of that section and switches to the accompanying @error block. The idea borrows from error boundaries in React.

Resources are a natural fit here. When a resource's loader fails, calling value() throws that error as an exception. That behavior is deliberate: had value() quietly returned an empty value on error, the failure would often slip by unnoticed. Consequently, templates up to now had to inspect the error() signal before touching value(). On the demo's luggage detail page, 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>
}

Angular 22.2 hands this responsibility to a @boundary block:

<!-- [...]/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 takes the resource as an input supplied by the router — more on that shortly. The template can now read the value directly, without an error check first. If value() throws, the boundary intercepts the exception and renders the @error block. Because the link back to the overview sits outside the boundary, it remains visible no matter what. In the demo, visiting the detail page with an invalid id, such as /luggage/9999, causes the loader to throw, and the page displays the message from the @error block.

A boundary may cover any portion of a template and also shields the components inside it: when a child component throws during rendering or change detection, that error likewise surfaces in the nearest enclosing boundary. Boundaries can be nested. Two caveats are worth noting: content projected via <ng-content> is not covered by the boundary, and an error raised inside the @error block itself bubbles up to the next outer boundary.

Working with Caught Errors

The @error block exposes the error through the implicit variable $error, much like $index in @for. You can rename it with let, and when allows multiple blocks tailored to different error kinds. For the optimistic case, there is also an implicit function $reset, which simply attempts the render 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 does not vanish silently into the fallback view. Angular additionally forwards it to the ErrorHandler — to its new optional onViewError method if that is implemented, otherwise to handleError. So without a custom ErrorHandler, the error still appears in the console.

Errors can also be intercepted when components and views are created dynamically: createComponent and the corresponding ViewContainerRef methods accept an onError callback. Apart from the error itself, both onViewError and onError receive an ErrorDetails object identifying the affected component and, when applicable, the catching boundary together with its reset function. Error boundaries are available 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 →

Data Loading via Router Resources

Resolvers fetch data before a route activates and hand back an Observable or Promise. Within Angular's Signal-centric model, that felt out of place. Router Resources address this and add a performance gain on top: the router kicks off every resource belonging to a navigation simultaneously, whereas resolvers are handled one after another down the route hierarchy. Suppose the parent route needs 2 seconds for its data and the child route 3 — with resolvers the user waits 5 seconds, while with Router Resources it is only 3 seconds, matching the slowest resource.

The feature is switched on with withRouterResources:

// src/app/app.config.ts

import {
  provideRouter,
  withComponentInputBinding,
  withRouterResources,
} from '@angular/router';

[...]

provideRouter(routes, withComponentInputBinding(), withRouterResources()),

A route declares its data through the resources property. The function executes in an injection context and is handed a ResourceContext exposing params, queryParams, fragment, and data as Signals. The route for editing a passenger fetches the record before the component shows up:

// 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 builds 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);
}

That resource is blocking: the router waits until the value exists, just as it did with a resolver. With withComponentInputBinding in place, the component receives the unwrapped value as an input — identical to what it got from the resolver previously. During the prereleases of 22.2, a resource carrying a default value did not block, since the router considered a value already present. The final release looks exclusively at the loading state, so a default value is no longer a problem.

Router Resources Without Blocking

Navigation should not always have to wait. With nonBlocking, the router activates the route right away and passes the still-loading resource as a whole to the component. The luggage detail page from the first section relies on precisely this:

// 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>>();
}

As a result, isLoading, error, and value are all available in the template, and the error boundary from the opening section handles the error case.

Router Resources offer more: through the ActivatedRoute, a resource can be reloaded without triggering a new navigation, and a RedirectCommand thrown inside the loader redirects to another route when data is missing. These details, along with the interaction with Signal Stores, are discussed in the article Router Resources: Loading Data with the Angular Router.

Further Changes at a Glance

Alongside the two headline features, error boundaries and Router Resources, Angular 22.2 brings a host of refinements touching the router, the compiler, testing, Signal Forms, and the CLI. The most interesting ones in brief:

  • Throwing RedirectCommand: Guards, resolvers, and Router Resources may now throw a RedirectCommand rather than return one. This simplifies redirects from deeply nested calls, for example inside a catch block.
  • Auto cleanup for route injectors is stable: withExperimentalAutoCleanupInjectors is now withAutoCleanupInjectors. The router uses it to tear down the environment injectors of routes that are no longer active. Custom RouteReuseStrategy implementations can participate in the decision through the new optional methods retrieveStoredRouteHandles and shouldDestroyInjector.
  • containsTree is public: This function checks whether one UrlTree is contained within another and takes the same options as isActive. Until now it was only usable internally.
  • Private members in templates: Templates may now reach properties and methods declared as private on the component. This eases work with isolatedDeclarations, since private members no longer require explicit type annotations.
  • strictUnclaimedEventNames: The new option in angularCompilerOptions flags event bindings that are neither an output of an applied directive nor a known DOM event — for example (userSelcted) instead of (userSelected). It is opt-in and applies to camelCase names.
  • TestBed.createDirective: Directives can be tested without writing a host component by hand. The method returns a DirectiveFixture that gives access to the directive instance and the host element. Inputs, outputs, and the host's tag name are configurable.
  • Injector via query: View and content queries can read the injector of the matched node with { read: Injector }. That is useful when dynamically created content needs the injector of a particular spot in the template.
  • WebMCP annotations: Tools an application exposes to AI agents via declareExperimentalWebMcpTool or provideExperimentalWebMcpTools now describe their behavior through annotations with the hints readOnlyHint, consequentialHint, and untrustedContentHint. Signal Forms exposed as tools via the experimentalWebMcpTool option get readOnlyHint and untrustedContentHint pinned to false, because a form by definition alters the page state.
  • Signal Forms: hidden without a condition: hidden(path) now hides a field permanently even without a when condition, consistent with readonly and disabled.
  • Animations: animate.enter and animate.leave now also accept functions and Signals as bindings.
  • CLI: The CLI's MCP server gains a --root option that limits access to a subtree in monorepos. --stats-json produces 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 now builds on oxc-parser, and numerous performance improvements accelerate builds, among them 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

Final Thoughts

Angular 22.2 is a minor release, yet two of its additions visibly shift daily work. Error boundaries give templates a mechanism other frameworks have had for some time, and they pair well with the Resource API, whose value() throws on error. Router Resources pull route data loading into the Signal world and eliminate the resolver waterfall. Both are labeled developer previews, but the trajectory is clear. On top of that come smaller enhancements such as throwing RedirectCommand, TestBed.createDirective, and a build that runs faster thanks to decoupled type checking.