Three developer-preview features that quietly change how you load data, isolate failures, and structure components
Have you ever opened a page and watched it flicker through an empty state, a spinner, and finally real data, all because the component had to manage its own loading? Angular 22.2 gives us a cleaner way to handle this with router resources, and it ships two other changes worth knowing.
Since Angular now ships only one major release per year, the minors carry real weight. This one brings router resources, error boundaries (both in developer preview), and access to private members from templates.
If you like practical Angular breakdowns written like a chat between devs, follow me here on Medium so you do not miss the next one.
What You Will Learn
By the end of this article, you will know how to:
- Load data in the router with router resources, so your component receives the final value
- Isolate failing child components with
@boundaryand@error - Use
privatemembers directly in templates, and why#privatefields no longer fit - Write unit tests for each of these features
Note: Before we dive into the examples, a quick note: the code snippets provided here are meant purely for understanding the concept. Some syntax shown may reflect patterns from earlier Angular versions, and the features covered here are in developer preview. Always refer to the official documentation for the most current API and syntax.
Feature 1: Angular Router Resources
The problem with loading inside the component
Imagine a holidays page. A service creates a resource, and the component consumes it. The resource only starts when the component is created, and the component must handle the loading state, where the value may not exist yet.
Here is the typical setup. First, a small service that exposes a resource:
// holiday.service.ts
import { Injectable } from '@angular/core';
import { httpResource } from '@angular/common/http';
export interface Holiday {
id: number;
title: "string;"
}
@Injectable({ providedIn: 'root' })
export class HolidayService {
load() {
return httpResource<Holiday[]>(() => '/api/holidays');
}
}
This service is a plain injectable that returns an HTTP resource. Nothing is router-specific yet.
Moving the resource into the router
With router resources, the router starts loading during navigation and blocks until the data is ready. Your component then gets the final value.
Define the resource on the route:
// app.routes.ts
import { inject } from '@angular/core';
import { Routes } from '@angular/router';
import { HolidayService } from './holiday.service';
import { HolidaysComponent } from './holidays.component';
export const routes: Routes = [
{
path: 'holidays',
component: HolidaysComponent,
resources: () => {
const holidayService = inject(HolidayService);
return {
holidays: holidayService.load(),
};
},
},
];
The object literal's key (holidays) is the name under which the loaded value reaches your component. The router also passes a context with params and query params as signals. That matters when a route ID changes, because you can feed that signal into the resource and it refreshes automatically.
Receiving the value as a plain input
// holidays.component.ts
import { Component, input } from '@angular/core';
import { Holiday } from './holiday.service';
@Component({
selector: 'app-holidays',
template: `
@for (holiday of holidays(); track holiday.id) {
<article>{{ holiday.title }}</article>
} @empty {
<p>No holidays found.</p>
}
`,
})
export class HolidaysComponent {
readonly holidays = input.required<Holiday[]>();
}
The component asks for a Holiday[], not a resource. It has no idea the data came from a resource, and the input is public because it is bound from outside.
Enabling the feature
You have to opt in explicitly in your app config:
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withRouterResources } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withRouterResources()),
provideHttpClient(),
],
};
Now navigate Home, then Holidays. Loading starts early, inside the router navigation itself.
Where do you load data today: in the component, in a resolver, or in a store? Tell me in the comments and I will cover the migration path in a follow-up.
Unit testing a component that receives router data
Because the component just takes an input, testing is simple. No router needed:
// holidays.component.spec.ts
import { TestBed } from '@angular/core/testing';
import { HolidaysComponent } from './holidays.component';
describe('HolidaysComponent', () => {
it('renders holidays passed in as input', async () => {
const fixture = TestBed.createComponent(HolidaysComponent);
fixture.componentRef.setInput('holidays', [
{ id: 1, title: "'Vienna' },"
{ id: 2, title: "'Lisbon' },"
]);
await fixture.whenStable();
const cards = fixture.nativeElement.querySelectorAll('article');
expect(cards.length).toBe(2);
expect(cards[0].textContent).toContain('Vienna');
});
it('shows the empty message when there are no holidays', async () => {
const fixture = TestBed.createComponent(HolidaysComponent);
fixture.componentRef.setInput('holidays', []);
await fixture.whenStable();
expect(fixture.nativeElement.textContent).toContain('No holidays found.');
});
});
The test uses setInput to simulate what the router will provide. That is a nice side effect of this design: the component stays easy to test.
Feature 2: Angular Error Boundaries
Why one broken child hurts everyone
Say the holidays page has two children, a title component and a holiday card. If one throws, the parent has a problem too. Here is a title component that fails on purpose:
// title.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-title',
template: `<h1>Holidays</h1>`,
})
export class TitleComponent {
constructor() {
throw new Error('Title failed to initialize');
}
}
With this in place, the holidays page shows nothing at all.
Isolating the failure with @boundary and @error
Wrap the risky child in a boundary in the template:
// holidays-page.component.ts
import { Component } from '@angular/core';
import { TitleComponent } from './title.component';
@Component({
selector: 'app-holidays-page',
imports: [TitleComponent],
template: `
@boundary {
<app-title />
} @error {
<h1>Holidays</h1>
}
<section>
<p>The rest of the page still renders.</p>
</section>
`,
})
export class HolidaysPageComponent {}
If app-title throws, the boundary catches it, the @error block acts as the fallback, and the rest of the page stays visible. There are more options, such as conditional catches, but this is the basic shape.
Testing the fallback
// holidays-page.component.spec.ts
import { TestBed } from '@angular/core/testing';
import { HolidaysPageComponent } from './holidays-page.component';
describe('HolidaysPageComponent', () => {
it('shows the fallback and keeps the page alive when the title throws', async () => {
const fixture = TestBed.createComponent(HolidaysPageComponent);
await fixture.whenStable();
const element: HTMLElement = fixture.nativeElement;
expect(element.querySelector('h1')?.textContent).toContain('Holidays');
expect(element.textContent).toContain('The rest of the page still renders.');
});
});
Because the child throws on purpose, this one test proves both the fallback and the isolation. Depending on how the test environment reports handled errors, you may need to adjust error handling in your test setup.
Feature 3: Private Members in Templates
Before 22.2, a template could only read members that were public or protected. That is why so much modern Angular code is full of protected.
// summary.component.ts
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-summary',
template: `<h2>{{ title() }}</h2>`,
})
export class SummaryComponent {
private readonly title = signal('Summer Holidays');
}
In 22.2, this just works. The reasoning from the Angular team is that the template and the TypeScript class form one unit, not two separate parts. Expect to see more private and fewer protected in modern Angular code.
There is one catch. ECMAScript private fields, the ones with hash notation, are private at runtime too, so the template cannot reach them:
export class SummaryComponent {
// This will NOT work in the template
#title = signal('Summer Holidays');
}
Switch to the TypeScript private keyword if the template needs the value.
Testing it
// summary.component.spec.ts
import { TestBed } from '@angular/core/testing';
import { SummaryComponent } from './summary.component';
describe('SummaryComponent', () => {
it('renders a private signal in the template', async () => {
const fixture = TestBed.createComponent(SummaryComponent);
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('h2').textContent)
.toContain('Summer Holidays');
});
});
Notice we test the rendered output, not the private member. That keeps tests focused on behavior.
Which of the three features will you adopt first? Vote in the comments: A) router resources, B) error boundaries, C) private in templates.
Bonus Tips
- Feed route params into resources as signals. When the ID in the URL changes, the resource refreshes on its own, with no manual subscriptions.
-
Keep fallbacks cheap. An
@errorblock should be simple. A fallback that can also fail defeats the purpose. - Place boundaries around risky areas only. Third-party widgets and optional panels are good candidates. Wrapping everything hides real bugs.
-
Migrate
protectedgradually. There is no need for a big-bang refactor. Switch toprivatewhen you touch a file anyway. - Skim the changelog. This release has more changes than the three covered here.
Recap
Angular 22.2 brings three notable changes. Router resources move data loading into the router, so your component receives the final value as a plain input. Error boundaries with @boundary and @error stop a single failing child from taking down the page. And private members can now be used in templates, reflecting that the template and class are one unit, though ECMAScript #private fields will not work there.
Both router resources and error boundaries have many more options than shown here. For the full picture, read the official documentation and the changelog, and look at the community's videos and articles. Related reading: [link to your related Angular article here].
If this helped you, share it with a teammate or on social media. It is the easiest way to help another dev find it.
What did you think?
Did this approach match how you are solving it, or do you have a different take? Drop a comment. I genuinely read every single one.
Found this helpful?
If this saved you even a few minutes of debugging or confusion, hit that clap button so others can find it too. It really does make a difference.
Want more tips like this?
I share one practical dev insight every week. Follow me here on Medium or subscribe to my newsletter so you never miss one.
Let us connect. Find me on LinkedIn or GitHub and let us keep the conversation going.
Follow Me for More Angular & Frontend Goodness:
I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.
- 💼 LinkedIn — Let’s connect professionally
- 🎥 Threads — Short-form frontend insights
- 🐦 X (Twitter) — Developer banter + code snippets
- 👥 BlueSky — Stay up to date on frontend trends
- 🌟 GitHub Projects — Explore code in action
- 🌐 Website — Everything in one place
- 📚 Medium Blog — Long-form content and deep-dives
- 💬 Dev Blog — Free Long-form content and deep-dives
- ✉️ Substack — Weekly frontend stories & curated resources
- 🧩 Portfolio — Projects, talks, and recognitions
- ✍️ Hashnode — Developer blog posts & tech discussions
- ✍️ Reddit — Developer blog posts & tech discussions
Top comments (0)