TL;DR — upgrade Angular 19 to 20 in 3 steps:
# Prerequisites: Node.js 20.11.1+ and TypeScript 5.8+ npm install -g @angular/cli@latest ng update @angular/cli@20 @angular/core@20 ng test # if it fails, see the Karma fix belowWhat actually breaks:
afterRender()is renamed toafterEveryRender()(no auto-migration),TestBed.get()is removed, and@angular-devkit/build-angularis replaced by@angular/build— which drops Karma. Every fix is covered step by step below.
1. The Full Story Behind Karma’s Removal: Beyond a Broken Build
The immediate issue when upgrading is that ng test will fail. The reason is a fundamental change in Angular’s build tools.
Why did Angular drop Karma?
With Angular 20, the default build package changes from @angular-devkit/build-angular to the new @angular/build. This new package no longer includes the Karma plugin used by legacy test setups.
The web ecosystem has moved on to faster test runners like Vitest and Jest that use modern tools like Vite and esbuild. Karma had become a bottleneck.
What the new world looks like (Vitest/Jest)
Angular’s experimental test runner, now powered by Vitest, is the future. Migrating means your unit tests will run in a fast, modern Node.js-based environment.
The “temporary fix” explained
npm install @angular-devkit/build-angular --save-devThis command forces the CLI to fall back to the old compiler that still supports Karma. It’s a compatibility bridge — but the message is clear: start planning your migration to Jest or Vitest soon.
How Do I Migrate from Karma to Vitest in Angular 20?
Run the official schematic instead of reinstalling the legacy builder:
ng generate @angular/core:karma-to-vitestThis auto-converts your test configuration and updates angular.json to use the @angular/build:unit-test builder with runner: 'vitest'. Standard Karma setups migrate cleanly; custom Webpack hacks in karma.conf.js need manual rewriting afterward.
2. Prerequisites
Before starting the update process, make sure you meet the following:
- Node.js: Version
20.11.1or later - TypeScript: Version
5.8or later - Project backup: Commit all current changes in Git
3. How to Update: CLI Command and Comparison
Update Command
Make sure you’re running Node.js v20.11.1 or later:
ng update @angular/cli @angular/coreIf you use Angular Material:
ng update @angular/cli @angular/core @angular/materialManual intervention required
To reinstall the old compiler with Karma support:
npm install @angular-devkit/build-angular --save-devThis wasn’t required in earlier versions but is now mandatory if you want to keep using Karma.
Compared to earlier upgrades
- Angular v19:
ng updatejust worked. - Angular v20: You must manually reinstall the old builder for Karma.
4. Control Flow: More Than Syntactic Sugar
@for replaces *ngFor and is a major improvement.
Old syntax
<div *ngFor="let item of items; trackBy: trackItemById">{{ item.name }}</div>New syntax
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
} @empty {
<div>No items to display.</div>
}trackis mandatory and encourages best practices.@emptyimproves DX by removing the need for separate@if.
Automated migration and performance
Use the CLI to automatically refactor templates to the new control flow syntax:
ng generate @angular/core:control-flowGeneral Best Practices and Further Migrations
You can also migrate to other modern Angular features:
ng generate @angular/core:standalone
ng generate @angular/core:inject
ng generate @angular/core:route-lazy-loading
ng generate @angular/core:signal-input-migration
ng generate @angular/core:signal-queries-migration
ng generate @angular/core:output-migrationThese commands allow for a comprehensive update of an Angular app to leverage the latest patterns.
5. Standalone by Default: A Fundamental Architectural Shift
By explicitly listing dependencies using the imports array at the component level, each component becomes self-contained. This:
- Clarifies your architecture
- Improves tree-shaking
- Results in smaller bundles
6. Zoneless: Escaping the “Magic” of Change Detection
In a zone-less world, the UI only updates when you explicitly tell it to.
Signals are the main tool here.
mySignal.set(newValue);This directly tells Angular to update only the DOM parts that use that signal. It’s a surgical, predictable, and high-performance approach.
7. New Naming Convention: Why It Feels Complicated
Angular 20 introduces a new official naming convention that drops traditional suffixes.
| Old naming | New naming | Intent |
|---|---|---|
user-profile.component.ts | user-profile.ts | UI component |
auth.service.ts | auth-store.ts | State management |
highlight.directive.ts | highlight.ts | Directive |
user-api.service.ts | user-api.ts | HTTP requests |
The goal is to focus on intent rather than technical type: a class that manages state is a “store,” one that makes HTTP requests is an “api.”
Naming rules
- Use dashes:
user-profile.ts - Match class and filename
- Keep
.spec.tsfor tests - Avoid generic names like
utils.ts - Co-locate related files
Feature-based folder structure
src/
├── core/
│ └── auth/
│ ├── auth-store.ts
│ ├── login.ts
│ └── register.ts
├── features/
│ └── users/
│ ├── user-profile.ts
│ ├── user-api.ts
│ └── user-settings.tsBenefits
- Cleaner Git diffs
- Intention-oriented code
- Easier onboarding
8. Important Detail: browserslist and Browser Support
Angular 20 no longer supports Opera officially.
If you list Opera in your browserslist, you may need to remove it.
Other non-mainstream browsers may also lose support, potentially triggering warnings or build issues.
On Angular 20 and ready for more? Continue with Angular 20 to 21: Breaking Changes and How to Fix Them, or see every migration path in the Angular Upgrade Guide.