Handling App Updates
When you deploy a new build of an Angular PWA, browsers that have already installed the service worker keep serving the old cached version until the worker updates in the background. Users can sit on a stale build for hours or days unless you actively detect a new version and prompt them to reload. Angular’s SwUpdate service gives you a clean, observable-driven API to notice new versions, ask the user to apply them, and recover gracefully when the cache becomes corrupted.
How service worker updates work
The Angular service worker checks for a new ngsw.json manifest whenever the app (re)loads and, by default, periodically while the tab is open. If the manifest changed, it downloads the new assets in the background and stages them. The crucial part: the new version does not activate until every open tab is reloaded. SwUpdate lets you intervene in that window — surfacing a prompt so the user reloads on demand instead of whenever they happen to close all tabs.
SwUpdate is only active in production builds where the service worker is registered. In ng serve it is disabled, so always guard your code with the isEnabled flag.
Detecting available updates
SwUpdate.versionUpdates is an Observable<VersionEvent> that emits as the worker moves through its lifecycle. The event you care about is VERSION_READY, which means a new version finished downloading and is ready to activate.
import { Component, inject, OnInit } from '@angular/core';
import { SwUpdate, VersionReadyEvent } from '@angular/service-worker';
import { filter } from 'rxjs/operators';
@Component({
selector: 'app-update-prompt',
standalone: true,
template: `
@if (updateAvailable()) {
<div class="update-banner" role="alert">
A new version is available.
<button (click)="applyUpdate()">Reload</button>
</div>
}
`,
})
export class UpdatePromptComponent implements OnInit {
private readonly swUpdate = inject(SwUpdate);
readonly updateAvailable = signal(false);
ngOnInit(): void {
if (!this.swUpdate.isEnabled) {
return;
}
this.swUpdate.versionUpdates
.pipe(filter((e): e is VersionReadyEvent => e.type === 'VERSION_READY'))
.subscribe(() => this.updateAvailable.set(true));
}
async applyUpdate(): Promise<void> {
await this.swUpdate.activateUpdate();
document.location.reload();
}
}
Remember to import
signalfrom@angular/core. Thefiltertype guard narrows the stream toVersionReadyEventso TypeScript knowscurrentVersionandlatestVersionare present.
Version event types
versionUpdates emits several event types as the download progresses. Knowing them helps you build richer UX and logging.
| Event type | Meaning |
|---|---|
VERSION_DETECTED | A new manifest hash was found; download is starting. |
VERSION_READY | New version fully downloaded and ready to activate. |
VERSION_INSTALLATION_FAILED | Download or installation of the new version failed. |
NO_NEW_VERSION_DETECTED | A manual checkForUpdate() found nothing new. |
You can log the transition between the current and incoming version:
this.swUpdate.versionUpdates
.pipe(filter((e): e is VersionReadyEvent => e.type === 'VERSION_READY'))
.subscribe((event) => {
console.log('Current version:', event.currentVersion.hash);
console.log('Available version:', event.latestVersion.hash);
});
Output:
Current version: 8f2c1a9b...
Available version: 3d77e0f4...
Polling for updates
By default the worker checks for updates on navigation and at intervals, but for long-lived single-page sessions you often want to poll explicitly. Call checkForUpdate() on a timer once the app has stabilized — checking before stabilization can keep the app from ever becoming stable.
import { inject, Injectable } from '@angular/core';
import { ApplicationRef } from '@angular/core';
import { SwUpdate } from '@angular/service-worker';
import { concat, interval } from 'rxjs';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class UpdateCheckService {
private readonly appRef = inject(ApplicationRef);
private readonly swUpdate = inject(SwUpdate);
start(): void {
if (!this.swUpdate.isEnabled) {
return;
}
const appStable$ = this.appRef.isStable.pipe(first((stable) => stable));
const everySixHours$ = interval(6 * 60 * 60 * 1000);
concat(appStable$, everySixHours$).subscribe(async () => {
try {
await this.swUpdate.checkForUpdate();
} catch (err) {
console.error('Update check failed:', err);
}
});
}
}
Call start() from your root component or an APP_INITIALIZER.
Recovering from a broken state
If the browser cache becomes corrupted — for example, an asset hash no longer matches the manifest — the service worker enters an unrecoverable state and emits on unrecoverableUpdates. The only fix is a hard reload to fetch everything fresh.
this.swUpdate.unrecoverableUpdates.subscribe((event) => {
console.error('Unrecoverable state:', event.reason);
document.location.reload();
});
Best practices
- Always guard
SwUpdateusage withif (!this.swUpdate.isEnabled) return;so the code is a no-op during development. - Prefer prompting the user over silently calling
activateUpdate()— an unexpected reload can lose unsaved form input. - Call
activateUpdate()and only thendocument.location.reload(), so the page reloads onto the already-activated new version. - Subscribe to
versionUpdatesonly after the worker is enabled, and use a typedfilterguard to handleVERSION_READYcleanly. - Run
checkForUpdate()afterApplicationRef.isStableemitstrue, never before, to avoid blocking app stabilization. - Handle
unrecoverableUpdateswith a full reload so a corrupted cache cannot leave users stranded on a broken app. - Keep your update banner accessible with
role="alert"so screen readers announce it.