Chrome Extension Extension Update Handling — Best Practices

5 min read

Extension Update Handling

Graceful handling of extension updates is critical for maintaining user data integrity and a seamless experience. This pattern covers the complete update lifecycle.

The onInstalled Event

Chrome provides chrome.runtime.onInstalled to detect extension lifecycle events:

chrome.runtime.onInstalled.addListener((details) => {
  switch (details.reason) {
    case 'install':
      // First-time installation
      initializeDefaultSettings();
      break;
    case 'update':
      // Extension was updated
      handleUpdate(details.previousVersion);
      break;
    case 'chrome_update':
      // Chrome itself was updated
      handleChromeUpdate();
      break;
  }
});

Detecting Version Changes

Compare the current manifest version with the stored previous version:

async function handleUpdate(previousVersion: string): Promise<void> {
  const currentVersion = chrome.runtime.getManifest().version;
  
  if (previousVersion === currentVersion) {
    return; // No meaningful version change
  }
  
  console.log(`Updating from ${previousVersion} to ${currentVersion}`);
  
  // Run sequential migrations
  await runMigrations(previousVersion, currentVersion);
  
  // Update stored version
  await chrome.storage.local.set({ extensionVersion: currentVersion });
}

Sequential Version Migrations

Run migrations in order to handle incremental changes:

interface Migration {
  version: string;
  migrate: () => Promise<void>;
}

const migrations: Migration[] = [
  { version: '2.1.0', migrate: migrateV210 },
  { version: '2.2.0', migrate: migrateV220 },
  { version: '3.0.0', migrate: migrateV300 },
];

async function runMigrations(fromVersion: string, toVersion: string): Promise<void> {
  for (const migration of migrations) {
    if (isVersionNewer(migration.version, fromVersion) && 
        !isVersionNewer(migration.version, toVersion)) {
      await migration.migrate();
    }
  }
}

Storage Schema Upgrades

Handle storage schema changes safely:

async function migrateV210(): Promise<void> {
  const data = await chrome.storage.local.get(['settings', 'userPrefs']);
  
  // Add new fields with defaults
  const upgradedSettings = {
    ...data.settings,
    theme: data.settings?.theme || 'system',
    notifications: true,
    lastUpdated: Date.now(),
  };
  
  // Rename keys if needed
  if ('userprefs' in data) {
    upgradedSettings.preferences = data.userprefs;
  }
  
  // Keep backup before migrating
  await chrome.storage.local.set({ 
    settings_backup_v210: data.settings 
  });
  
  await chrome.storage.local.set({ settings: upgradedSettings });
}

Content Script Reconnection

After updates, existing content scripts may fail with “Receiving end does not exist”:

async function reconnectContentScripts(): Promise<void> {
  const tabs = await chrome.tabs.query({ active: true, status: 'complete' });
  
  for (const tab of tabs) {
    if (tab.id && isExtensionPage(tab.url)) {
      // Re-inject content scripts
      await chrome.scripting.executeScript({
        target: { tabId: tab.id },
        files: ['content.js'],
      });
    }
  }
}

Preserving User State

Always backup user data before migrations:

async function withRollback(
  operation: () => Promise<void>
): Promise<void> {
  const storage = await chrome.storage.local.get(null);
  const backupKey = `backup_${Date.now()}`;
  
  try {
    // Create backup
    await chrome.storage.local.set({ [backupKey]: storage });
    
    // Perform operation
    await operation();
    
    // Clear old backups (keep last 3)
    await cleanupOldBackups();
  } catch (error) {
    // Rollback on failure
    const backup = await chrome.storage.local.get(backupKey);
    await chrome.storage.local.set(backup[backupKey]);
    throw error;
  } finally {
    // Remove immediate backup after success or manual rollback
    await chrome.storage.local.remove(backupKey);
  }
}

Testing Updates Locally

Use chrome.management.setEnabled or reload the extension in developer mode:

// Reload extension after making changes
async function reloadExtension(): Promise<void> {
  const extInfo = await chrome.management.get(chrome.runtime.id);
  await chrome.management.setEnabled(extInfo.id, false);
  await chrome.management.setEnabled(extInfo.id, true);
}

Summary

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.