Chrome Runtime API Complete Reference
23 min readChrome Runtime API Complete Reference
The chrome.runtime API is the backbone of every Chrome extension. It provides lifecycle management, messaging between contexts, resource URL resolution, and platform utilities. No permission is required – it is available in every extension context (service worker, popup, options page, content script).
Properties
chrome.runtime.id
const extensionId: string = chrome.runtime.id;
// "abcdefghijklmnopqrstuvwxyz"
The globally unique identifier for this extension. Stable across sessions; changes only if the extension is unpacked and reloaded from a different directory.
chrome.runtime.lastError
chrome.runtime.lastError: { message: string } | undefined;
Set inside callbacks when the preceding async API call failed. In MV3 with promise-based APIs, prefer try/catch instead.
// Callback style (MV2 / backward-compat)
chrome.tabs.create({ url: "chrome://invalid" }, (tab) => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError.message);
return;
}
console.log("Tab created:", tab.id);
});
// Promise style (MV3) -- lastError not needed
try {
const tab = await chrome.tabs.create({ url: "https://example.com" });
} catch (err) {
console.error((err as Error).message);
}
chrome.runtime.getManifest()
function getManifest(): chrome.runtime.Manifest;
Returns the parsed manifest.json as an object. Useful for reading your own version, permissions, or custom keys at runtime.
const manifest = chrome.runtime.getManifest();
console.log(manifest.version); // "1.2.3"
console.log(manifest.name); // "My Extension"
console.log(manifest.permissions); // ["storage", "alarms"]
chrome.runtime.getURL(path)
function getURL(path: string): string;
Converts a relative path within the extension bundle to a fully qualified chrome-extension:// URL.
const popupUrl = chrome.runtime.getURL("popup.html");
// "chrome-extension://abcdef.../popup.html"
// Use in content scripts to reference extension-bundled assets
const img = document.createElement("img");
img.src = chrome.runtime.getURL("icons/logo.png");
document.body.appendChild(img);
Events
chrome.runtime.onInstalled
chrome.runtime.onInstalled.addListener(
callback: (details: {
reason: "install" | "update" | "chrome_update" | "shared_module_update";
previousVersion?: string;
id?: string;
}) => void
): void;
Fires when the extension is first installed, updated to a new version, or when Chrome itself updates. The standard place to run one-time setup.
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === "install") {
chrome.storage.local.set({ version: chrome.runtime.getManifest().version });
chrome.tabs.create({ url: chrome.runtime.getURL("onboarding.html") });
}
if (details.reason === "update") {
console.log(`Updated from ${details.previousVersion}`);
migrateStorageSchema(details.previousVersion!);
}
// Context menus must be recreated on every install/update
chrome.contextMenus.create({ id: "main-action", title: "Do the thing", contexts: ["selection"] });
});
chrome.runtime.onStartup
chrome.runtime.onStartup.addListener(callback: () => void): void;
Fires when a Chrome profile that has this extension installed starts up. Does not fire on install – only on subsequent browser launches.
chrome.runtime.onStartup.addListener(() => {
chrome.storage.session.set({ sessionStart: Date.now() });
});
chrome.runtime.onSuspend
chrome.runtime.onSuspend.addListener(callback: () => void): void;
Fires just before the service worker (MV3) or event page (MV2) is unloaded. You have roughly five seconds to run synchronous cleanup – async operations are not guaranteed to complete.
chrome.runtime.onSuspend.addListener(() => {
chrome.storage.session.set({ cachedData: inMemoryCache });
});
chrome.runtime.onMessage
chrome.runtime.onMessage.addListener(
callback: (
message: any,
sender: chrome.runtime.MessageSender,
sendResponse: (response?: any) => void
) => boolean | undefined
): void;
Fires when a message is sent via chrome.runtime.sendMessage or chrome.tabs.sendMessage from any context within the same extension. Return true to keep the channel open for async sendResponse.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "GET_DATA") {
fetchData(message.key)
.then((data) => sendResponse({ success: true, data }))
.catch((err) => sendResponse({ success: false, error: err.message }));
return true; // async -- must return true
}
if (message.type === "LOG") {
console.log(`[Tab ${sender.tab?.id}] ${message.text}`);
sendResponse({ ack: true }); // sync -- no return true needed
}
});
MessageSender fields:
| Field | Type | Description |
|---|---|---|
sender.tab |
Tab \| undefined |
The tab that sent the message (undefined from popup/background) |
sender.frameId |
number |
Frame ID within the tab (0 = main frame) |
sender.id |
string |
Extension ID of the sender |
sender.url |
string |
URL of the sending context |
sender.origin |
string |
Origin of the sending context |
sender.documentId |
string |
UUID of the sender document |
sender.documentLifecycle |
string |
Lifecycle state of the sender document |
chrome.runtime.onMessageExternal
chrome.runtime.onMessageExternal.addListener(
callback: (
message: any,
sender: chrome.runtime.MessageSender,
sendResponse: (response?: any) => void
) => boolean | undefined
): void;
Fires when a message arrives from a different extension or from a web page (if "externally_connectable" is configured in the manifest). The sender.id identifies which extension sent the message.
// manifest.json: "externally_connectable": { "ids": ["other-ext-id"] }
chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
if (sender.id !== "expected-extension-id") {
sendResponse({ error: "unauthorized" });
return;
}
sendResponse({ data: "shared info" });
});
chrome.runtime.onConnect
chrome.runtime.onConnect.addListener(
callback: (port: chrome.runtime.Port) => void
): void;
Fires when a long-lived connection is opened via chrome.runtime.connect().
chrome.runtime.onConnect.addListener((port) => {
port.onMessage.addListener((msg) => {
if (msg.type === "PING") port.postMessage({ type: "PONG", ts: Date.now() });
});
port.onDisconnect.addListener(() => {
if (chrome.runtime.lastError) console.warn("Port error:", chrome.runtime.lastError.message);
});
});
chrome.runtime.onConnectExternal
chrome.runtime.onConnectExternal.addListener(
callback: (port: chrome.runtime.Port) => void
): void;
Like onConnect, but for connections originating from other extensions or web pages. Requires "externally_connectable" in the manifest.
chrome.runtime.onConnectExternal.addListener((port) => {
console.log(`External connection from: ${port.sender?.id}`);
port.onMessage.addListener((msg) => {
port.postMessage({ echo: msg });
});
});
Manifest declaration for external messaging:
{
"externally_connectable": {
"matches": ["https://example.com/*"],
"ids": ["other-extension-id-here"]
}
}
chrome.runtime.onUpdateAvailable
chrome.runtime.onUpdateAvailable.addListener(
callback: (details: { version: string }) => void
): void;
Fires when a Chrome Web Store update is available but not yet applied. Chrome delays updates while the extension is active. Call chrome.runtime.reload() to apply immediately, or let Chrome apply it on next restart.
chrome.runtime.onUpdateAvailable.addListener((details) => {
console.log(`Update to v${details.version} available`);
// Apply immediately or defer -- see "Update flow" pattern below
chrome.runtime.reload();
});
Methods
chrome.runtime.sendMessage()
function sendMessage<R = any>(
message: any,
options?: { includeTlsChannelId?: boolean }
): Promise<R>;
function sendMessage<R = any>(
extensionId: string,
message: any,
options?: { includeTlsChannelId?: boolean }
): Promise<R>;
Sends a one-shot message to the extension’s service worker. When extensionId is provided, sends to that other extension.
// To own background
const response = await chrome.runtime.sendMessage({ type: "GET_COUNT" });
// To another extension
const extRes = await chrome.runtime.sendMessage("other-ext-id", { type: "SHARE" });
// Error handling
try {
await chrome.runtime.sendMessage({ type: "ACTION" });
} catch (err) {
// "Could not establish connection. Receiving end does not exist."
console.error((err as Error).message);
}
chrome.runtime.connect()
function connect(connectInfo?: { name?: string }): chrome.runtime.Port;
function connect(
extensionId: string,
connectInfo?: { name?: string }
): chrome.runtime.Port;
Opens a long-lived message channel returning a Port for bidirectional communication.
const port = chrome.runtime.connect({ name: "data-stream" });
port.postMessage({ subscribe: "updates" });
port.onMessage.addListener((msg) => console.log("Received:", msg));
port.onDisconnect.addListener(() => console.log("Closed"));
port.disconnect(); // close when done
chrome.runtime.getContexts() (MV3 only, Chrome 116+)
function getContexts(filter: {
contextTypes?: ContextType[];
documentIds?: string[];
documentOrigins?: string[];
documentUrls?: string[];
frameIds?: number[];
incognito?: boolean;
tabIds?: number[];
windowIds?: number[];
}): Promise<ExtensionContext[]>;
type ContextType =
| "TAB" | "POPUP" | "BACKGROUND"
| "OFFSCREEN_DOCUMENT" | "SIDE_PANEL"
| "DEVELOPER_TOOLS";
Returns information about active extension contexts. The MV3 replacement for getBackgroundPage().
// Check if an offscreen document exists
const offscreen = await chrome.runtime.getContexts({ contextTypes: ["OFFSCREEN_DOCUMENT"] });
const hasOffscreen = offscreen.length > 0;
// Check if popup is open
const popup = await chrome.runtime.getContexts({ contextTypes: ["POPUP"] });
const popupIsOpen = popup.length > 0;
chrome.runtime.getBackgroundPage() (MV2 only)
function getBackgroundPage(): Promise<Window>;
Returns the Window object of the background page. Not available in MV3. Replace with sendMessage() or shared modules.
const bg = await chrome.runtime.getBackgroundPage(); // MV2 only
bg.someGlobalFunction();
### chrome.runtime.reload() {#chromeruntimereload}
```typescript
function reload(): void;
Reloads the extension immediately. Equivalent to clicking the reload button on chrome://extensions. Terminates the service worker, re-reads the manifest, and restarts.
// Apply a pending update
chrome.runtime.onUpdateAvailable.addListener(() => {
chrome.runtime.reload();
});
chrome.runtime.requestUpdateCheck()
function requestUpdateCheck(): Promise<{
status: "throttled" | "no_update" | "update_available";
version?: string;
}>;
Asks the Chrome Web Store whether an update is available for this extension. Throttled if called too frequently.
const { status, version } = await chrome.runtime.requestUpdateCheck();
if (status === "update_available") {
console.log(`Version ${version} is available`);
}
if (status === "throttled") {
console.log("Too many checks -- try again later");
}
chrome.runtime.setUninstallURL()
function setUninstallURL(url: string): Promise<void>;
Sets a URL to open when the user uninstalls the extension. Must be http: or https:, max 1023 characters. Typically used for exit surveys.
chrome.runtime.onInstalled.addListener(() => {
chrome.runtime.setUninstallURL(
"https://example.com/uninstall-survey?ext=my-extension"
);
});
chrome.runtime.openOptionsPage()
function openOptionsPage(): Promise<void>;
Opens the extension’s options page as defined by options_ui in the manifest. If the page is already open in a tab, that tab is focused instead.
await chrome.runtime.openOptionsPage();
chrome.runtime.getPlatformInfo()
function getPlatformInfo(): Promise<{
os: "mac" | "win" | "android" | "cros" | "linux" | "openbsd" | "fuchsia";
arch: "arm" | "arm64" | "x86-32" | "x86-64" | "mips" | "mips64";
nacl_arch: "arm" | "x86-32" | "x86-64" | "mips" | "mips64";
}>;
Returns the operating system and architecture the extension is running on.
const platform = await chrome.runtime.getPlatformInfo();
if (platform.os === "mac") {
console.log("Use Cmd key shortcuts");
} else {
console.log("Use Ctrl key shortcuts");
}
chrome.runtime.getPackageDirectoryEntry()
function getPackageDirectoryEntry(): Promise<DirectoryEntry>;
Returns a DirectoryEntry (File System API) for the extension’s install directory.
const dir = await chrome.runtime.getPackageDirectoryEntry();
dir.getFile("data/config.json", {}, (entry) => {
entry.file((file) => {
const reader = new FileReader();
reader.onload = () => console.log(reader.result);
reader.readAsText(file);
});
});
Common Patterns
Install handler with storage migration
chrome.runtime.onInstalled.addListener(async (details) => {
const currentVersion = chrome.runtime.getManifest().version;
if (details.reason === "install") {
await chrome.storage.local.set({ version: currentVersion, settings: { theme: "auto" } });
await chrome.runtime.setUninstallURL("https://example.com/feedback");
}
if (details.reason === "update") {
const { version: old } = await chrome.storage.local.get("version");
// Run migrations based on previous version
if (old === "1.0.0") {
const { config } = await chrome.storage.local.get("config");
await chrome.storage.local.set({ settings: config });
await chrome.storage.local.remove("config");
}
await chrome.storage.local.set({ version: currentVersion });
}
});
Message router
type Handler = (payload: any, sender: chrome.runtime.MessageSender) => Promise<any>;
const handlers: Record<string, Handler> = {
GET_DATA: async (p) => ({ data: await fetchFromApi(p.endpoint) }),
SAVE: async (p) => { await chrome.storage.sync.set({ [p.key]: p.value }); return { ok: true }; },
TAB_URL: async (_, s) => ({ url: s.tab?.url ?? null }),
};
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
const handler = handlers[msg.type];
if (!handler) return sendResponse({ error: `Unknown: ${msg.type}` });
handler(msg.payload, sender)
.then(sendResponse)
.catch((err) => sendResponse({ error: err.message }));
return true; // async
});
Port management with reconnection
// content.ts -- auto-reconnecting port
function createPort(): chrome.runtime.Port {
const port = chrome.runtime.connect({ name: "content-link" });
port.onMessage.addListener((msg) => handleBackgroundMessage(msg));
port.onDisconnect.addListener(() => {
// Service worker went idle -- reconnect after a delay
setTimeout(() => { activePort = createPort(); }, 1000);
});
return port;
}
let activePort = createPort();
Deferred update flow
let updatePending = false;
chrome.runtime.onUpdateAvailable.addListener((details) => {
updatePending = true;
chrome.runtime.sendMessage({ type: "UPDATE_AVAILABLE", version: details.version }).catch(() => {});
});
chrome.idle.onStateChanged.addListener((state) => {
if (state === "idle" && updatePending) chrome.runtime.reload();
});
Error Handling Reference
| Method | Error Message | Cause |
|---|---|---|
sendMessage |
Could not establish connection. Receiving end does not exist. |
No onMessage listener in the target context |
sendMessage |
The message port closed before a response was received. |
Listener did not return true for async response |
connect |
Could not establish connection. |
Target context does not exist or has no onConnect listener |
sendMessage (ext) |
Invalid extension id |
The target extension is not installed |
setUninstallURL |
Invalid URL |
URL exceeds 1023 chars or uses a non-http(s) scheme |
openOptionsPage |
No options page |
options_ui not declared in the manifest |
getBackgroundPage |
Access denied |
Called from a content script, or called in MV3 |
MV2 vs MV3 Differences
| Feature | MV2 | MV3 |
|---|---|---|
| Background context | Persistent background page | Ephemeral service worker |
getBackgroundPage() |
Returns background Window |
Not available – use getContexts() |
getContexts() |
Not available | Returns info on all active contexts |
| API style | Callbacks + chrome.runtime.lastError |
Promises + try/catch |
onSuspend timing |
Fires when event page goes idle | Fires when service worker terminates (~30s idle) |
| Port lifetime | Survives as long as background page lives | Disconnects when service worker goes idle |
getPackageDirectoryEntry |
Full access (DOM available) | Available but limited (no DOM in service worker) |
Key migration note: In MV3, service workers terminate after approximately 30 seconds of inactivity. Long-lived Port connections will disconnect when this happens. Content scripts must handle reconnection, and all persistent state must go into chrome.storage rather than global variables.
Related
- Alarms API – scheduled background work
- Storage API Deep Dive – persistence patterns
- Tabs API –
sendMessageto content scripts - Chrome runtime API docs – official reference
Frequently Asked Questions
How do I communicate between background and content scripts?
Use chrome.runtime.sendMessage() from content scripts and chrome.runtime.onMessage.addListener() in the background.
How do I get the extension ID?
Use chrome.runtime.id to get your extension’s unique ID at runtime.
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.