DEV Community

Quang Phan
Quang Phan

Posted on

Manifest V3 Migration Mistakes That Will Cost You Hours (And How to Avoid Them)

Manifest V3 Migration Mistakes That Will Cost You Hours (And How to Avoid Them)

Migrating a Chrome extension from Manifest V2 to V3 isn't just flipping a flag — it's rethinking how your extension lives inside the browser. After reviewing dozens of migration attempts, certain mistakes surface again and again. Here's what trips most developers up, and how to sidestep them.

The Service Worker Trap

The biggest mental shift in MV3 is the background model. V2 used persistent background pages; V3 uses ephemeral service workers. Sounds simple, but it changes everything.

// ❌ V2 — this won't work in MV3
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  const data = fetchFromServer(); // async, but never awaited
  sendResponse({ data }); // sends undefined!
});
Enter fullscreen mode Exit fullscreen mode
// ✅ V3 — return a Promise
chrome.runtime.onMessage.addListener((request, sender) => {
  return fetchFromServer().then(data => ({ data }));
});
Enter fullscreen mode Exit fullscreen mode

The key rule: always return a Promise from message listeners. sendResponse is synchronous and fires before your async operation completes.

The 30-Second Timeout Problem

Service workers in MV3 shut down after ~30 seconds of inactivity. Any long-running task gets killed. If you're syncing data or making multiple API calls, use chrome.alarms or setTimeout with keepalive:

chrome.alarms.create('syncData', { periodInMinutes: 15 });
chrome.alarms.onAlarm.addListener(() => {
  // This keeps your worker alive during the alarm
  syncExtensionData();
});
Enter fullscreen mode Exit fullscreen mode

Content Script Injection Changes

In V2, you could inject scripts whenever you liked. In V3, content scripts are declarative and there are two worlds:

World Access Limitations
Main world Full DOM + page JS variables No chrome API access
Isolated world Full DOM + chrome APIs Sandboxed from page JS
// V3 — inject into main world for DOM access
chrome.scripting.executeScript({
  target: { tabId: tab.id },
  world: 'MAIN',
  files: ['content.js']
});
Enter fullscreen mode Exit fullscreen mode

Choose MAIN when you need to read/write page variables. Keep ISOLATED (default) when you need chrome APIs but want to stay sandboxed from the page.

Permission Frightening (Incremental Host permissions help)

V3 made permissions stricter. Exact host permissions are now required instead of <all_urls>. This is actually good for security, but it breaks migrations that relied on blanket access.

//  Old V2 approach  too broad
"permissions": ["tabs", "http://*/*", "https://*/*"]

//  V3 approach  request only what you need
"permissions": ["tabs"],
"host_permissions": ["https://example.com/*"]
Enter fullscreen mode Exit fullscreen mode

If you need to request permissions at runtime, use chrome.permissions.request() and explain to users exactly why:

chrome.permissions.request(
  { origins: ['https://example.com/*'] },
  granted => {
    if (granted) {
      // Permission is yours
    } else {
      // Gracefully degrade — don't break the extension
    }
  }
);
Enter fullscreen mode Exit fullscreen mode

CSP Restrictions Are Real

Content Security Policy in MV3 extensions is enforced differently. Inline <script> tags won't execute, and eval() is blocked.

<!-- ❌ Won't work in MV3 -->
<script>
  chrome.runtime.sendMessage('hello');
</script>
Enter fullscreen mode Exit fullscreen mode

Move all logic to JS files:

// content.js — loaded via chrome.scripting.executeScript
document.addEventListener('DOMContentLoaded', () => {
  console.log('Extension running on:', location.href);
});
Enter fullscreen mode Exit fullscreen mode

Debugging Service Workers

Since service workers don't have a visible UI, debugging is different:

  1. Open chrome://extensions
  2. Find your extension → Service Worker link
  3. Use console.log and the Background page DevTools just like V2
  4. For state persistence issues, remember: every restart wipes in-memory state. Use chrome.storage instead of global variables for anything that needs to survive restarts.
// ❌ Loses state on service worker restart
let cachedData = null;

// ✅ Persists across restarts
chrome.storage.local.get(['cachedData'], result => {
  cachedData = result.cachedData;
});
Enter fullscreen mode Exit fullscreen mode

The Verdict

MV3 migration isn't trivial, but it's worth it. You get better security, cleaner architecture, and automatic compliance with Chrome Web Store requirements. The most common pitfalls — message handler patterns, service worker timeouts, content script worlds, and CSP — are all solvable once you know they're coming.

If you're looking for tools to speed up your Chrome extension workflow during or after migration, ExtensionBooster has free utilities that handle ID lookups, manifest validation, and more — built specifically for extension developers.


Tags: chrome-extension, javascript, developer-tools, programming, productivity

Top comments (0)