
Angular 22.2 brings router resources and error boundaries, both released under developer preview. On top of that, TypeScript private members can now be reached from a component's template.
- Ng-news - Episode 26 21 Angular 22 2 (duplicated) - StackBlitz
- GitHub - ng-news/episode-26-21-angular-22-2: ng-news 26/21: Angular 22.2 router resources, error boundaries, and private template members. Runnable in StackBlitz.
Router Resources
Data fetching driven by the router is something other frameworks have offered for years. Angular shipped resolvers long ago, but it is good to see the framework now cover this in the “Angular way” — through the resource feature.
Resources are how modern Angular loads data asynchronously from a backend. Because instantiating a resource, or calling helpers built on it such as rxResource, kicks off loading right away, the usual pattern is to create the resource inside a component — or have the component receive it through an Angular service.
The consequence is that somebody has to write the code dealing with the resource's state. Is it still loading, has it been initialized, and so on?
Below is the HolidaysPage component, which 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();
}holiday.service.ts (resource factory)
createHolidaysResource() {
return resource({
loader: async () => {
await new Promise((resolve) => setTimeout(resolve, 1200));
return holidays;
},
});
}The complete service is available in StackBlitz.
Router resources offer a neat way to cut that boilerplate down.
A resource can be declared at the router level, where it fetches its data as soon as the user navigates to that route. By default it also blocks route activation, so the router renders the component only after the resource has fetched successfully. The fetched data arrives in 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() };
},
},
];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[]>();
}Note that the holidays input lines up with the resource name in the route configuration. Two features still need to be switched on: router resources and component input binding.
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()),
],
};When several resources are declared, whether across nested routes or for a single route, their data loads in parallel, which shortens the wait for the end user.
Additional options exist for router resources as well. They can be non-blocking, created asynchronously, and more. 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.
In TypeScript, catching such errors with try/catch to keep them contained is, of course, possible for obvious reasons.
For the demonstration, a Title component throws an unhandled error. Once it is used as a child, its parent view suffers too. In this case the holiday cards 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');
}
}holidays-page.ts (template)
<app-title />
@for (holiday of holidays(); track holiday.id) {
<app-holiday-card [holiday]="holiday" />
}
Error boundaries transfer the idea of a TypeScript try/catch into the template.
Wrapping the risky template in a @boundary template block confines the error to that area. Title therefore cannot do any further damage to the rest of the application.
The catch counterpart is @error, which accepts a fallback template:
holidays-page.ts (template)
@boundary {
<app-title />
} @error {
<h1>Holidays</h1>
}
@for (holiday of holidays(); track holiday.id) {
<app-holiday-card [holiday]="holiday" />
}Further options are available, among them access to the error, resetting a boundary, and choosing a fallback conditionally. A static fallback suffices for this example. In the companion demo, select Broken title to see the fallback take over while the holiday cards remain visible.
Angular error boundaries documentation
Private Members in Templates
The third feature is a small adjustment to the component class. TypeScript private members are now reachable 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');
}Prior to Angular 22.2, a component member — property or method — consumed by the template typically had to be public or protected. protected was chosen when the member was intended for the template rather than the component's public API, even though subclasses could still reach it.
With private now on the table as well, I would recommend it for members used solely by the component and its template. Conceptually, a component's template and its TypeScript class form a single unit, and reaching a private member from the template simply expresses that idea.
Access to TypeScript private members in templates consequences · Issue #70514 · angular/angular
One distinction matters here. TypeScript's private modifier is not the same thing as ECMAScript # fields. A field that is private at runtime, such as #title, cannot be accessed by the template directly. For state that ought to stay inside the class, it still makes sense.
Going forward, then, Angular code should use TypeScript's private for members the template needs to reach, purely for the sake of consistency.
protected, by contrast, will likely fade out of template-only usage or be reserved mainly for class inheritance.
Ng-News 26/21: Angular 22.2 — Router Resources, Error Boundaries, and Private Template Members was originally published in ng-news on Medium, where people are continuing the conversation by highlighting and responding to this story.
