Angular 22.2 ships router resources and error boundaries, both marked as developer preview. On top of that, TypeScript private members are now reachable from a component's template.

The demo application referenced throughout this article lives here:

Data Fetching Through Router Resources

Loading data as part of navigation is something other frameworks have offered for years. Angular had resolvers before, but it is good to see first-class support arriving in idiomatic Angular style, meaning through the resource primitive.

Resources are how modern Angular pulls data from the backend asynchronously. Creating a resource, or building on top of it with helpers such as rxResource, kicks off loading right away, which is why resources usually live in a component or reach the component indirectly through an Angular service.

That arrangement also puts the burden of handling resource state on you: is it loading, has it been initialized, and so on.

The component HolidaysPage below receives its resource from a HolidayService:

holidays-page.ts

import { Component, inject } from '@angular/core';
import { HolidayCard } from './holiday-card';
import { HolidayService } from './holiday.service';
import { Title } from './title';

@Component({
  selector: 'app-holidays',
  imports: [HolidayCard, Title],
  template: `
    <app-title />
    @if (holidays.isLoading()) {
      <p>Loading holidays...</p>
    } @else if (holidays.error()) {
      <p>Could not load the holidays.</p>
    } @else if (holidays.hasValue()) {
      @for (holiday of holidays.value(); track holiday.id) {
        <app-holiday-card [holiday]="holiday" />
      }
    }
  `,
})
export class HolidaysPage {
  readonly holidays = inject(HolidayService).createHolidaysResource();
}
Enter fullscreen mode Exit fullscreen mode

holiday.service.ts (resource factory)

createHolidaysResource() {
  return resource({
    loader: async () => {
      await new Promise((resolve) => setTimeout(resolve, 1200));
      return holidays;
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

The complete service can be found in StackBlitz.

Router resources cut through that complexity nicely.

A resource can be declared at the router level so that it fetches data when the user navigates to the matching route. By default the router also blocks route activation: the component is rendered only after the resource has successfully fetched its data. The fetched value reaches the component through input binding, as though it had always been there.

holidays-routes.ts

import { inject } from '@angular/core';
import { Routes } from '@angular/router';
import { HolidayService } from './holiday.service';
import { HolidaysPage } from './holidays-page';

export const routes: Routes = [
  {
    path: 'holidays',
    component: HolidaysPage,
    resources: () => {
      const holidayService = inject(HolidayService);
      return { holidays: holidayService.createHolidaysResource() };
    },
  },
];
Enter fullscreen mode Exit fullscreen mode

holidays-page.ts

import { Component, input } from '@angular/core';
import { Holiday } from './holiday';
import { HolidayCard } from './holiday-card';
import { Title } from './title';

@Component({
  selector: 'app-holidays',
  imports: [HolidayCard, Title],
  template: `
    <app-title />
    @for (holiday of holidays(); track holiday.id) {
      <app-holiday-card [holiday]="holiday" />
    }
  `,
})
export class HolidaysPage {
  readonly holidays = input.required<Holiday[]>();
}
Enter fullscreen mode Exit fullscreen mode

The name of the holidays input lines up with the resource name in the route configuration. Router resources and component input binding both need to be enabled:

app.config.ts

import { ApplicationConfig } from '@angular/core';
import {
  provideRouter,
  withComponentInputBinding,
  withRouterResources,
} from '@angular/router';
import { routes } from './holidays-routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withRouterResources(), withComponentInputBinding()),
  ],
};
Enter fullscreen mode Exit fullscreen mode

When several resources are declared, whether across nested routes or for a single route, their data is fetched in parallel, which also shortens loading time for the user.

Router resources expose additional options as well. Among other things, they can be non-blocking or be created asynchronously. The official documentation covers the details.

Error Boundaries

An unhandled error thrown during rendering or change detection can travel up the component hierarchy and bring rendering to a halt.

TypeScript already gives us try/catch, so errors can of course be caught and isolated there.

To demonstrate, the Title component throws an unhandled error. Used as a child component, it also harms the parent view. In this case the holiday cards would never render and HolidaysPage remains empty.

import { Component } from '@angular/core';

@Component({
  selector: 'app-title',
  template: '<h1>Holidays</h1>',
})
export class Title {
  constructor() {
    throw new Error('The title could not be created');
  }
}
Enter fullscreen mode Exit fullscreen mode

holidays-page.ts (template)

<app-title />

@for (holiday of holidays(); track holiday.id) {
  <app-holiday-card [holiday]="holiday" />
}
Enter fullscreen mode Exit fullscreen mode

Error boundaries carry the try/catch concept from TypeScript over into the template.

Wrapping the risky template in a @boundary block confines the error to that region. As a result, Title no longer disrupts the rest of the application.

The catch counterpart is @error, where a fallback template can be supplied:

holidays-page.ts (template)

@boundary {
  <app-title />
} @error {
  <h1>Holidays</h1>
}

@for (holiday of holidays(); track holiday.id) {
  <app-holiday-card [holiday]="holiday" />
}
Enter fullscreen mode Exit fullscreen mode

Further options exist: reading the error, resetting a boundary, and picking a fallback conditionally. A static fallback suffices for this example. In the companion demo, choose Broken title to observe the fallback while the holiday cards stay on screen.

Error boundaries with @boundary • Angular

The web development framework for building modern apps.

favicon angular.dev

Private Members Available in Templates

The third change concerns the component class and is a small one. TypeScript private members are now accessible from the template:

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-private-title',
  template: '<h1>{{ title() }}</h1>',
})
export class PrivateTitle {
  private readonly title = signal('Holidays');
}
Enter fullscreen mode Exit fullscreen mode

Prior to Angular 22.2, a component member (property or method) referenced by the template would typically be declared public or protected. protected was the common choice when a member was intended for the template rather than the component's public API, even though subclasses could still reach it.

With private now permitted, it is the option I would recommend for members used solely by the component and its template. Conceptually, a component's template and its TypeScript class form a single unit, and touching a private member from the template reflects that view.

Angular team discussion: the component and its template form one unit

One distinction matters here. TypeScript's private modifier is not the same as ECMAScript # fields. A field that is private at runtime, such as #title, cannot be accessed directly from the template. It remains appropriate for state that should stay inside the class.

For consistency, future Angular code should use TypeScript's private for members the template needs to reach.

protected, by contrast, will likely fade from template-only usage or be reserved mainly for class inheritance.