Skip to content
Angular ng pwa 4 min read

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 signal from @angular/core. The filter type guard narrows the stream to VersionReadyEvent so TypeScript knows currentVersion and latestVersion are present.

Version event types

versionUpdates emits several event types as the download progresses. Knowing them helps you build richer UX and logging.

Event typeMeaning
VERSION_DETECTEDA new manifest hash was found; download is starting.
VERSION_READYNew version fully downloaded and ready to activate.
VERSION_INSTALLATION_FAILEDDownload or installation of the new version failed.
NO_NEW_VERSION_DETECTEDA 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 SwUpdate usage with if (!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 then document.location.reload(), so the page reloads onto the already-activated new version.
  • Subscribe to versionUpdates only after the worker is enabled, and use a typed filter guard to handle VERSION_READY cleanly.
  • Run checkForUpdate() after ApplicationRef.isStable emits true, never before, to avoid blocking app stabilization.
  • Handle unrecoverableUpdates with 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.
Last updated June 14, 2026
Was this helpful?