Rorix Technologies
Engineering13 min read

Angular v17 → v22: The Migration Guide That Includes the Breakages

Angular17AngularAngular18Angular19Angular20Angular21Angular22FrontendTypeScriptCodingWebDevelopment
Angular v17 → v22: The Migration Guide That Includes the Breakages
On this page21 sections

If you are running Angular 17, 18, or 19 in production today, you are running software that receives no security patches. Angular 17 stopped getting them in May 2025. Angular 18 in November 2025. Angular 19 in May 2026.

That is the short version, and for a lot of teams it is the whole conversation. But "you should upgrade" is useless advice without an honest answer to "what will it actually cost," and most migration guides do not give you one. They tell you to run ng update and wish you luck.

I have run this migration. This is what the guides leave out.


The support clock

Angular ships a major version every six months. Through v21, each one got 18 months of support: six months active, twelve months of security-only LTS. Starting with v22, that widened to 24 months, and Angular's docs now hint the six-month cadence itself is under review.

VersionReleasedSecurity support endsWhere that leaves you
v17Nov 202315 May 2025End of life
v18May 202421 Nov 2025End of life
v19Nov 202419 May 2026End of life
v20May 202528 Nov 2026LTS, expiring soon
v21Nov 202530 Jun 2027LTS
v223 Jun 2026~30 Jun 2028Active

The gap between "we're only a couple of versions behind" and "we are unsupported" is smaller than most teams assume. Angular 17 felt current in early 2024. It was dead eighteen months later.

This is not a theoretical risk

Three CVEs were disclosed across the Angular platform in late 2025. HeroDevs, who sell extended support for end-of-life Angular and therefore have an interest in you being worried, documented them:

  • CVE-2025-59052 (CVSS 7.1)  - a race condition in the SSR pipeline that could leak request-scoped data between concurrent requests
  • CVE-2025-66035 (CVSS 7.7) - HttpClient leaking XSRF tokens via protocol-relative URLs
  • CVE-2025-66412 - stored XSS in the template compiler, related to SVG and MathML attribute sanitization

The CVE identifiers check out independently. Teams on end-of-life versions got patches for none of them.

If you are in a regulated industry, or you fill out security questionnaires for enterprise customers, this is the line item that eventually forces the decision anyway. Better to make it on your schedule.


What it costs

You cannot jump straight from 17 to 22. The migration schematics are written to run between adjacent majors, so 17 → 22 means five sequential upgrades. Skipping versions means skipping the schematics that handle them, and you find out months later when something renders wrong.

Published estimates vary wildly, and it is worth knowing who is saying what:

SourceEstimateWhat it's good for
Arc (recruiting site)v19/v20 → current: 2-8 hours. v17/v18: 3-6 daysBest case. Small, clean, well-tested app.
FrontendMinds (agency, 19 enterprise projects)1-3 days per major for small apps, 1–-2 weeks for medium enterpriseReasonable planning baseline
OctoPerf (published their actual numbers)7,786 files, ~381,000 lines. Three weeks of framework work after a failed first attempt. One commit rewrote 5,221 files.What large actually looks like

The first two are marketing. The third is someone showing their working.

My own experience: In one of my large scale enterprize project it will take 2-3 week to complete one major version upgrade. For a real enterprise application with a themed Material UI, a handful of ngx-* dependencies, and a test suite that stopped being maintained around 2022, one to two weeks per major hop is a fair planning number.

But the version bumps are not where the money goes.

The four things that actually cost money

Almost every migration overrun I have seen traces back to one of these, and none of them is a version number.

1. Angular Material M2 → M3 theming. The official schematic produces output that looks broken. This is a design project wearing an upgrade costume, and it needs design QA time you probably did not budget.

2. The Webpack to esbuild builder switch. Painless if your build config is stock. Weeks if you have custom Webpack plugins, ngx-build-plus, or Module Federation, because none of that transfers automatically.

3. The zoneless and OnPush transition, and the test fallout behind it. fakeAsync and tick do not survive going zoneless. Neither does Karma, eventually.

4. Third-party library lag. PrimeNG, NgRx, and the long tail of ngx-* packages set your ceiling. Angular is rarely the blocker. Your dependencies are.


The distinction nobody makes: must-do versus optional

This is the part I wish someone had told me first. Upgrading Angular is not the same thing as adopting modern Angular. Confusing the two is how a two-week upgrade turns into a two-quarter rewrite that nobody approved.

Must do - the build breaks without it

RequirementWhich hopWhy
Node and TypeScript bumpsEvery hopBuild fails immediately
standalone: false on NgModule-declared componentsv19Default flipped
Drop this. on template context variablesv19Silent compiler behavior change
Rename afterRender()afterEveryRender()v20No automatic migration exists
Replace TestBed.flushEffects()v20Removed
Accept the ChangeDetectionStrategy.Eager migrationv22Preserves existing behavior
Audit route param readsv22Inheritance default changed
Fix duplicate input bindingsv22Now a compile error
Material and CDK in lockstep with coreEvery hopPeer deps are pinned

Optional - genuinely optional, whatever the release notes imply

ModernizationAvailable fromWhen to do it
Signalsv18 stableIncrementally, in new code
Control flow (@if / @for)v17Its own ticket, not mid-upgrade
inject() over constructor injectionv14Opportunistically
Zoneless change detectionv20 stable, v21 default for new appsAfter hot paths are on OnPush
Vitest over Karmav21 default for new appsWhen the suite is stable
Signal Formsv22 stablev22 is the green light
Angular Ariav22 stableOnly for new accessible components
Selectorless componentsv22No urgency
v20 filename conventionsv20Never, if you don't want to. It's a style guide.

The rule I give teams: during a version hop, do only the first table. Everything in the second gets its own ticket, shipped independently. A migration PR that also converts four hundred templates to @if is a PR nobody can review, and unreviewed migration PRs are how regressions reach production.


The plan

Five phases. Each one ships to production on its own. If a phase cannot ship alone, it is too big.

Phase 0 - Assessment (one sprint, no code changes)

Inventory every third-party Angular dependency and map its supported version window. This determines your ceiling before you write a line of code. Flag PrimeNG, NgRx, Material theming, @angular-builders/custom-webpack, ngx-build-plus, Module Federation, i18n, and anything still on @nguniversal/*.

Then audit your test coverage honestly. If the suite is thin or flaky, stop and fix that first. The team at OctoPerf put it well after their migration: their bottleneck was validation, not transformation. They relied on 10,919 unit tests to verify 5,221 changed files, because reviewing those files by hand was never going to happen.

Check Node and TypeScript versions in four places, not one: local dev, CI images, Dockerfiles, and the engines field.

Phase 1 - Get to a supported version (v17 → v20)

v17 → v18 is the quiet hop. Mostly deprecations. You will be prompted to migrate the builder - decline for now if you have a custom Webpack config, and handle it separately in Phase 2.

v18 → v19 is the standalone flip, and the most consequential single change in the range. The migration adds standalone: false to everything currently declared in an NgModule. It mostly works. Afterwards, manually verify every dynamic component load and every template that used this.foo, because v19 changed what that means and the compiler will not warn you.

v19 → v20 requires two manual renames the schematic will not do for you: afterRender() becomes afterEveryRender(), and TestBed.flushEffects() is gone.

Ship this. You are now on supported software, and the security argument is satisfied. Everything past this point is engineering value rather than risk mitigation, which makes it a much easier conversation with whoever holds the budget.

Phase 2 - The hard workstreams (separately, any order)

The builder migration. Material M2 → M3. The control flow migration. Any third-party overhaul like PrimeNG's theming rewrite or @nguniversal/*@angular/ssr.

Each of these is its own project with its own QA. Bundling them into the version hops is how migrations stall out and get quietly abandoned.

Phase 3 - v20 → v22

v20 → v21 makes zoneless and Vitest the defaults for new apps. Your existing app keeps zone.js and Karma. Do not change both at once.

v21 → v22 requires Node 22+ and TypeScript 6.0+ before you run ng update, not after. The build fails immediately otherwise. Check your CI images and Dockerfiles, not just your laptop.

Let the Eager migration run and do not fight it. Then audit anything reading route params, because the router now inherits params from all parent routes by default.

Phase 4 - Modernization (ongoing, forever)

Peel components off Eager onto OnPush. Move hot paths to signals. Enable zoneless once OnPush coverage is good. Migrate Karma to Vitest. Adopt Signal Forms in new code.

None of this is urgent. All of it is worth doing eventually.


What breaks, version by version

v18 → v19

standalone: true became the default. Every component, directive, and pipe is standalone unless you say otherwise.

The migration handles most of it, but two issues on the Angular tracker are worth knowing about. In #58747, ng update failed to remove the flag from components without imports. In #57829, dynamic component loading via ViewContainerRef.createComponent and createNgModule broke for components relying on the implicit old default.

The one that costs people real debugging time: this.foo in a template no longer refers to a template context variable. If you meant the template variable, drop the this.. Nothing errors. The value is just wrong.

Also in v19: TypeScript below 5.5 dropped, effect() timing changed to run as part of change detection rather than as a microtask, and event replay became stable and on by default for new SSR apps.

v19 → v20

Signals largely finished. effect(), linkedSignal(), and toSignal() all went stable.

Two manual fixes, no schematic: afterRender() renamed to afterEveryRender(), and TestBed.flushEffects() removed.

*ngIf, *ngFor, and *ngSwitch were officially deprecated in favour of control flow. Structural directives in general were not deprecated, only those three. The style guide also changed filename conventions and started recommending [class.x] over ngClass. All of that is optional.

TypeScript 5.8 and Node 20 became required.

v20 → v21

Zoneless became the default for new apps, and zone.js is no longer shipped with them. Existing apps are not forced off it. Vitest replaced Karma and Jasmine as the default runner, though Karma still works. Signal Forms arrived as experimental, Angular Aria as developer preview, and the CLI gained a built-in MCP server with an onpush_zoneless_migration tool.

The trap: because Vitest always runs zoneless in Angular, fakeAsync and tick cannot be used with it. There is also a documented regression (angular-cli#32121) where fakeAsync tests fail with zone-testing.js is needed for the fakeAsync() test helper but could not be found under the new unit-test builder, even when you are still using Karma.

v21 → v22

Released 3 June 2026. Four things break.

OnPush is now the default change detection strategy. A component that does not set changeDetection means OnPush, not check-always. Nothing in your existing code breaks if you let the migration work - ng update stamps the new ChangeDetectionStrategy.Eager value onto every component that relied on the old default. What changes is that every new component your team generates is OnPush. If people still write code assuming mutation-based updates will show up, v22 is where that habit stops working.

Route params now inherit from all parent routes. The paramsInheritanceStrategy default changed from 'emptyOnly' to 'always'. A child route reading a param name that also exists on a parent may now resolve to a value it never saw before, and code with defensive "if this param is undefined" branches may silently take the other path.

You can revert it globally

provideRouter(routes, withRouterConfig({
  paramsInheritanceStrategy: 'emptyOnly',
}))

But renaming colliding params (:userId instead of a second :id) is the better fix. The new default is genuinely more useful for deep-linked detail pages.

Node 22 and TypeScript 6.0 are mandatory. Node 20 is dropped. Node 26 is supported.

And three smaller edges: duplicate input bindings on one element are now a compile error rather than silently tolerated, data-* attributes bind as attributes instead of property bindings, and the HTTP transfer cache skips credentialed requests by default so authenticated responses cannot leak across SSR sessions.

On the plus side, v22 is where the modern stack stops being experimental. Signal Forms, resource() and httpResource(), Angular Aria, injectAsync(), and HttpClient with Fetch all went stable. Incremental hydration became the default. Selectorless components arrived.


Where ng update leaves you on your own

The schematics handle maybe seventy percent of this mechanically. The remaining thirty is where the budget goes.

Control flow migration is good but needs a human reading every diff. It drops i18n attributes on ng-template (#53289, #52517), which silently breaks translations with no error. It mishandles ng-template combined with [ngIf] and let- (#53291). It leaves orphaned named templates behind (#58850). And it introduces whitespace around inline elements, which corrupts rendered text in ways that only show up when someone reads the page.

Peer dependency conflicts stop ng update cold, usually with Migration failed: Incompatible peer dependencies found. Your options, ranked honestly:

  1. overrides in package.json. Explicit, reviewable, documented. Use this.
  2. --legacy-peer-deps. Acceptable. Leaves the conflict unresolved but breaks nothing.
  3. --force. Avoid. It can leave you with duplicate package versions in node_modules and runtime errors that make no sense.

Whatever you use, follow it with a full build, a serve, and the test suite.

Material M2 → M3 is the worst gap between what the docs promise and what you get. It has its own post in this series, because it deserves one.


What you actually get

The build speed argument is the easiest one to make to a CTO, because Angular measured it themselves. The v17 release reported up to 87% faster ng build, an 80% faster edit-refresh loop, and enterprise partners seeing 67% build-time improvements on the esbuild pipeline.

Beyond that: dropping zone.js removes roughly 33KB raw from the bundle and eliminates a large share of unnecessary change-detection cycles. Incremental hydration and event replay improve Time to Interactive without losing interactions that happen before hydration. And modern Angular is what people you hire already know - a codebase full of NgModule wiring takes longer to onboard into, every single time.


The honest summary

Angular's release notes describe v18 as having few breaking changes and present the standalone and Material migrations as automated. The issue trackers tell a different story, particularly for M3 theming, where the official documentation was internally contradictory enough that developers filed issues about the documentation itself.

That gap is the whole point of this article. ng update is genuinely good. It is not the plan.

If you are on 17, 18, or 19, the first move is not "adopt signals" or "go zoneless." It is Phase 1: get to a supported version, ship it, and stop being an audit finding. Everything else can wait for a quarter when it fits.


We do this work. If you have an Angular codebase that has fallen behind and you want a realistic scope before you commit budget to it, contact us.

Work with Rorix

Need engineers who stay?

Dedicated developers at $3,500 per developer per month, working your backlog on 2-week sprints with a named technical lead.

Angular v17 → v22: The Migration Guide That Includes the Breakages