Angular 22.2: new features for developers

ℹ️ Draft: Angular 22.2 is currently available as a pre-release. This post will be updated when new features are added.

The development of Angular 22.2 just started. I will update this pages with the changes and the examples.

Until the new release you can refer to:

Angular 22.1: new features for developers
Angular 22.1.0 was released on July 28, 2026. It’s a bugfix release, but we can find some minor new features added to the framework. Here the full list of changes: https://github.com/angular/angular/releases/tag/v22.1.0 The most interesting changes for application developers

and

Angular, Node, Typescript version compatibility and new features
Compatibility Matrix Angular, Node and TypeScript

Changes until today (September 10, 2026)

WebMCP improved features

Angular 22 introduced the support to WebMCP (as experimental). This features helps AI Agents to use your web application, creating web AI-aware apps.

Annotations interface

We can now add metadata to MCP tools and declare them trustable
readOnlyHint?: boolean; this tells the AI agent that the tool wont modify the state (no changes in the UI)

readOnlyHint?: boolean; this tells if the tool will return untrusted content.

Example:

declareExperimentalWebMcpTool({
  name: 'getUsers',
  description: 'Returns users',
  inputSchema: {
    type: 'object',
    properties: {},
  },

  annotations: {
    readOnlyHint: true,
    untrustedContentHint: false,
  },

  execute: () => {
    return userService.getUsers();
  },
});

New @boundary error handling

feat(core): introduce `@boundary` control flow and programmatic error handling by JeanMeche · Pull Request #70463 · angular/angular
Introduces a declarative way to catch rendering errors in templates using the new @boundary block, preventing local failures from crashing the entire application hierarchy. @boundary { <comple…

This is a big change, you can now catch rendering errors.

Declarative way:

@boundary {
  <user-profile />
} @error (let error = $error) {
  <p>{{ error.message }}</p>
  <button (click)="$retry()">Retry</button>
}

Programmatic:

viewContainerRef.createComponent(UserProfileComponent, {
  onError: (error, details) => {
    console.error(error);
  },
});

Here you can find the definition of BoundaryError.

Injector directly from a query

feat(core): allow reading Injector from a view or content query
From the description: `{read: Injector}` now returns the node injector of the matched element, so a component can resolve tokens as they are seen from that element. This is useful when wrapping third-party components that project templates and expect directives inside those templates to inject the host component.

Now you can read an Injector directly from a query.

Until 22.2 you needed a workaround.

@Directive({
  selector: '[injectorRef]',
})
export class InjectorRefDirective {
  readonly injector = inject(Injector);
}

.html
<div #box injectorRef></div>

.ts
injectorRef = viewChild(InjectorRefDirective);

boxInjector = computed(
  () => this.injectorRef()?.injector
);

Now you can get the Injector of a specific element directly from a query.

boxInjector = viewChild('box', { read: Injector })

and use it to resolve things like services, InjectionToken, directives/providers attached to that element and objects visible from that node, e.g.:

const element = injector?.get(ElementRef);

Route resource

Router Resources

feat(router): expose router resources in public API

With the new Route Resources we can use a resource()instead of a resolver to fetch the data, reducing the complexity of the code. Additionally, we can define blocking and not blocking routing and .reload() the data without navigating to the route again.
Until now:

export const userResolver: ResolveFn<User> = (route) => {
  return inject(UserService).getUser(route.paramMap.get('id')!);
};

const routes: Routes = [
  {
    path: 'users/:id',
    component: UserProfile,
    resolve: {
      user: userResolver,
    },
  },
];

With Router Resources:

const routes: Routes = [
  {
    path: 'users/:id',
    component: UserProfile,
    resources: (ctx) => ({
      user: resource({
        params: () => ctx.params()['id'],
        loader: ({ params: id }) =>
          inject(UserService).getUser(id),
      }),
    }),
  },
];

Testing Directives

feat(core): add utility for testing directives
If you create tests for your UI and you create directives, this change can simplify your code. Now you can test a directive without creating a temporary host component using TestBed.createDirective

Your directive:

@Directive({
  selector: '[highlight]',
  host: { class: 'highlighted' },
})
class HighlightDirective {}

Until now:

@Component({
  template: `<div highlight></div>`,
  imports: [HighlightDirective],
})
class TestHost {}

const fixture = TestBed.createComponent(TestHost);
fixture.detectChanges();

With the change:

const fixture = TestBed.createDirective(
  HighlightDirective,
  { tagName: 'div' }
);

fixture.detectChanges();

expect(fixture.directiveInstance)
  .toBeInstanceOf(HighlightDirective);

Component only members are now accessible to the template

feat(compiler): allow template to access private props
The component's own template is allowed to access the component's private members.

export class AppComponent {
  private title = signal('Dashboard');

  private refresh() {
    // only called from the template
  }
}

<h1>{{ title() }}</h1>
<button (click)="refresh()">Refresh</button>

Previously titleand refresh where not accessible in the view.

RedirectCommand in Guards and Resolvers

feat(router): allow throwing RedirectCommand to trigger redirects

Angular guards and resolvers can now throw RedirectCommand to redirect immediately, making redirects from deeply nested logic cleaner and avoiding RedirectCommand or UrlTree pollution in function return types.

The Angular team extended RedirectCommand with Error:
export class RedirectCommand extends Error

const guard: CanActivateFn = () => {
  const router = inject(Router);

  if (!isLoggedIn()) {
    throw new RedirectCommand(
      router.parseUrl('/login')
    );
  }

  return true;
};

Previously we could not throw the Redirect command:

return new RedirectCommand(
      router.parseUrl('/login')
);

Other changes