Updating and Clearing Notifications

Manage the lifecycle of chrome.notifications in an MV3 extension: stable notification IDs, updating instead of stacking, clearing on action or timeout, requireInteraction, getAll after worker restarts, and the notification center.

Published October 2, 2026 Updated October 2, 2026 7 min read
Table of Contents

A sync extension shows “Syncing…”, then “Synced 12 items”, then “Sync failed”, then “Synced 3 items” — as four separate notifications that pile up in the operating system’s notification center, the earliest still claiming a sync is in progress. Each chrome.notifications.create call without a stable ID adds a new notification, and nothing removes the old ones. Notifications have a lifecycle — create, update, clear — and an extension that only ever creates them turns a useful alert into clutter users learn to ignore. This guide manages that lifecycle in an MV3 service worker, including after the worker restarts. It belongs to notifications, badges and the action API.

How notification identity works

chrome.notifications.create(notificationId, options) shows a notification under the given ID. If a notification with that ID already exists from your extension, the call replaces it — same slot, new content. Omit the ID and Chrome generates a unique one each time, which is what causes stacking. notifications.update(id, options) changes fields of an existing notification and resolves false if it no longer exists (the user dismissed it, or the OS removed it). notifications.clear(id) removes it. notifications.getAll() returns the IDs currently shown by your extension, which is how a restarted service worker learns what is on screen. onClosed fires with byUser when a notification goes away. On most platforms Chrome uses the system notification center, so notifications may persist after the popup or even the browser window is gone.

One notification per job, updated in placeA sync job uses the stable ID sync-status: it is created as Syncing, updated to Synced 12 items, and cleared automatically after a timeout or when the user clicks it; a new job reuses the same ID.create('sync-status')"Syncing…"update('sync-status')"Synced 12 items"clear('sync-status')timeout or clicknext sync reuses the IDcreate('sync-status')replaces any leftoveronClosed(byUser)record dismissalgetAll()after worker restart
Stable IDs turn a pile of alerts into one evolving message.

Step-by-step: notification lifecycle

1. Choose stable IDs by purpose

1// notify.js
2export const IDS = {
3  sync: "sync-status",
4  update: "extension-updated",
5  download: (id) => `download-${id}`,
6  reminder: (itemId) => `reminder-${itemId}`,
7};

Execution context: a shared module in the service worker. One ID per thing the user cares about: a single slot for sync status, one per download, one per reminder. Encoding the entity ID in the notification ID lets click handlers find the entity without extra storage.

2. Create or replace in one call

 1export async function showSync(state, detail) {
 2  const opts = {
 3    type: "basic",
 4    iconUrl: "icons/128.png",
 5    title: chrome.i18n.getMessage(`sync_${state}_title`),
 6    message: detail,
 7    priority: state === "failed" ? 1 : 0,
 8    silent: state !== "failed",
 9  };
10  await chrome.notifications.create(IDS.sync, opts);
11}

Execution context: the service worker. Calling create with an existing ID replaces the content, so the code does not need to know whether a previous sync notification is still showing. Only failures make a sound and get raised priority; routine success should be quiet, or not shown at all, as discussed in notification permissions and alert fatigue.

Notification calls and when to use themcreate with a new ID, create with an existing ID, update, clear and getAll compared on effect and the right use.CallEffectUse forcreate(newId)New notificationDistinct eventscreate(existingId)Replaces contentStatus that changesupdate(id)Partial change; false if goneProgress ticksclear(id)Removes itResolved or acted ongetAll()IDs on screenRecovery after restart
create-with-ID is the workhorse; update is for partial changes to something you know is showing.

3. Update without resurrecting dismissed notifications

1export async function setDownloadProgress(id, percent) {
2  const stillShown = await chrome.notifications.update(IDS.download(id), { progress: percent });
3  if (!stillShown) return;            // user dismissed it — respect that, don't recreate
4}

Execution context: the service worker. update resolves false when the notification no longer exists. For frequent updates, that is the signal the user does not want to watch this one — recreating it with create would undo their dismissal. Use create only for a new state the user should see, such as completion or failure. See progress notifications for long tasks.

4. Clear notifications when they are resolved

1chrome.notifications.onClicked.addListener(async (id) => {
2  if (id === IDS.sync) await chrome.tabs.create({ url: chrome.runtime.getURL("library.html#sync") });
3  if (id.startsWith("reminder-")) await openItem(id.slice("reminder-".length));
4  await chrome.notifications.clear(id);
5});
6
7export async function onSyncResolved() {
8  await chrome.notifications.clear(IDS.sync);   // the error was fixed elsewhere — remove the stale alert
9}

Execution context: the service worker. Clicking a notification does not always remove it from the notification center; clearing it explicitly after acting is cleaner. Equally important: when the condition the notification described goes away (the sync recovered, the reminder was completed in the popup), clear it so the notification center does not show stale problems.

Clearing a stale errorSync fails and the worker shows an error notification; later the user fixes the connection and a sync succeeds; the worker clears the error notification instead of leaving it in the notification center.Service workerNotification centerUsercreate('sync-status', 'Sync failed')shownfixes network; next sync OKclear('sync-status')
When the problem goes away, so should the alert.

5. Time out transient notifications yourself

1export async function showTransient(id, opts, ms = 8000) {
2  await chrome.notifications.create(id, { ...opts, requireInteraction: false });
3  await chrome.alarms.create(`clear-notif:${id}`, { when: Date.now() + ms });
4}
5chrome.alarms.onAlarm.addListener(({ name }) => {
6  if (name.startsWith("clear-notif:")) chrome.notifications.clear(name.slice("clear-notif:".length));
7});

Execution context: the service worker. System notification centers keep notifications around after the banner disappears. For informational messages that are useless after a few seconds (“Copied to clipboard”), clear them. A setTimeout would be lost if the worker stopped, so use an alarm (Chrome rounds very short alarm delays up to its minimum interval, so for sub-30-second clearing a setTimeout while the worker is awake is acceptable as a best effort). Conversely, requireInteraction: true keeps important notifications on screen until acted on — use it sparingly.

6. Recover after a service worker restart

1chrome.runtime.onStartup.addListener(reconcile);
2async function reconcile() {
3  const shown = Object.keys(await chrome.notifications.getAll());
4  const { activeDownloads = [] } = await chrome.storage.session.get("activeDownloads");
5  for (const id of shown) {
6    if (id.startsWith("download-") && !activeDownloads.includes(id.slice(9))) chrome.notifications.clear(id);
7  }
8}

Execution context: the service worker. Notifications can outlive the worker and even the browser session on some platforms. On startup, compare what is shown with what is actually in progress, and clear orphans — a “Downloading 40%” notification with no download behind it is worse than none.

7. Track dismissals to calibrate

1chrome.notifications.onClosed.addListener(async (id, byUser) => {
2  if (!byUser) return;
3  const kind = id.split("-")[0];
4  const { dismissals = {} } = await chrome.storage.local.get("dismissals");
5  dismissals[kind] = (dismissals[kind] ?? 0) + 1;
6  await chrome.storage.local.set({ dismissals });
7});

Execution context: the service worker. If users dismiss a kind of notification over and over without clicking, it is noise. Use local counts to decide whether to suggest turning that kind off, or to stop showing it by default.

Common mistakes

  • No notification ID. Every call stacks a new notification.
  • Recreating after dismissal. Ignores the user’s choice.
  • Leaving stale errors. Clear notifications when their condition resolves.
  • setTimeout for clearing long delays. Lost when the worker stops.
  • requireInteraction on routine messages. Forces users to dismiss noise.

Cross-browser variation

  • Chrome / Edge: IDs, update, clear, getAll, onClosed as described; on macOS and Windows notifications go to the system notification center.
  • Firefox: supports create, clear, getAll, onClicked, onClosed; update is not supported and only the basic type is fully supported — recreate with the same ID instead of updating.
  • Safari: no notifications API for extensions; use the badge or in-page UI, or the containing app’s notifications.

Verification

  1. Trigger four sync states in a row and confirm only one notification exists.
  2. Dismiss a progress notification and confirm later progress ticks do not bring it back, but completion does.
  3. Restart the browser mid-download and confirm orphaned notifications are cleared on startup.
  4. Resolve an error elsewhere and confirm its notification disappears.

FAQ

Is there a limit to how many notifications an extension can show?

Platforms limit how many are visible at once; the rest queue or collapse. Stable IDs avoid hitting the limit.

Do notifications need a permission?

Yes, notifications. It does not trigger an install warning in Chrome.

Can I check whether notifications are blocked?

chrome.notifications.getPermissionLevel() returns granted or denied, reflecting whether the user blocked the extension’s notifications.

What happens to notifications when the extension is updated?

Existing notifications may remain while the extension restarts. Run the same reconciliation from onInstalled with the update reason, so notifications from the previous version that no longer match current state are cleared.

Other UI/UX Patterns & Interactive Components Resources