Chrome Extension Messaging Quickstart — Developer Guide

9 min read

Messaging Quickstart

Overview

@theluckystrike/webext-messaging provides fully-typed, promise-based message passing between extension contexts (background, content scripts, popups).

Install

npm install @theluckystrike/webext-messaging

Step 1: Define Your Message Map

Create a MessageMap type mapping message names to { request, response } shapes:

type Messages = {
  getUser: {
    request: { id: number };
    response: { name: string; email: string };
  };
  saveSettings: {
    request: { theme: string; fontSize: number };
    response: { success: boolean };
  };
  ping: {
    request: void;
    response: "pong";
  };
};
import { createMessenger } from "@theluckystrike/webext-messaging";

const msg = createMessenger<Messages>();

The Messenger<M> interface has three methods:

Step 3: Handle Messages (Background)

// background.ts
import { createMessenger } from "@theluckystrike/webext-messaging";

type Messages = {
  getUser: {
    request: { id: number };
    response: { name: string; email: string };
  };
  ping: {
    request: void;
    response: "pong";
  };
};

const msg = createMessenger<Messages>();

// Simulated user fetch
async function fetchUser(id: number) {
  return { name: "John Doe", email: "john@example.com" };
}

const unsubscribe = msg.onMessage({
  getUser: async (payload, sender) => {
    // payload typed as { id: number }
    const user = await fetchUser(payload.id);
    return { name: user.name, email: user.email };
  },
  ping: () => "pong",
});

Key points:

Step 4: Send Messages (Content Script / Popup)

// content.ts
import { createMessenger } from "@theluckystrike/webext-messaging";

type Messages = {
  getUser: {
    request: { id: number };
    response: { name: string; email: string };
  };
  ping: {
    request: void;
    response: "pong";
  };
};

const msg = createMessenger<Messages>();

const user = await msg.send("getUser", { id: 42 });
// user typed as { name: string; email: string }

const pong = await msg.send("ping", undefined);
// pong typed as "pong"

Step 5: Send to Specific Tabs (Background -> Content Script)

const result = await msg.sendTab(
  { tabId: 123 },
  "saveSettings",
  { theme: "dark", fontSize: 14 }
);

// With frame targeting
const result2 = await msg.sendTab(
  { tabId: 123, frameId: 0 },
  "ping",
  undefined
);

TabMessageOptions: { tabId: number; frameId?: number }

Step 6: Error Handling with MessagingError

import { createMessenger, MessagingError } from "@theluckystrike/webext-messaging";

type Messages = {
  getUser: {
    request: { id: number };
    response: { name: string; email: string };
  };
};

const msg = createMessenger<Messages>();

try {
  const user = await msg.send("getUser", { id: 1 });
} catch (err) {
  if (err instanceof MessagingError) {
    console.error("Messaging failed:", err.message);
    console.error("Original error:", err.originalError);
  }
}

MessagingError wraps chrome.runtime.lastError. Has .originalError for the underlying cause.

Step 7: Using Low-Level Functions

import { sendMessage, sendTabMessage, onMessage } from "@theluckystrike/webext-messaging";

type Messages = {
  getUser: {
    request: { id: number };
    response: { name: string; email: string };
  };
  ping: {
    request: void;
    response: "pong";
  };
};

// Send to extension (background)
const user = await sendMessage<Messages, "getUser">("getUser", { id: 1 });

// Send to specific tab
const result = await sendTabMessage<Messages, "ping">(
  { tabId: 123 },
  "ping",
  undefined
);

// Register handlers (returns unsubscribe)
const unsub = onMessage<Messages>({
  ping: () => "pong",
});

Step 8: Complete Example — Tab Manager Extension

This example demonstrates a complete extension with shared message types, a background script handler, and a popup sender.

Shared Types (src/types.ts)

// Shared message types used by both background and popup
export type Messages = {
  getActiveTab: {
    request: void;
    response: { id: number; title: string; url: string } | null;
  };
  getTabTitle: {
    request: { tabId: number };
    response: string;
  };
  closeTab: {
    request: { tabId: number };
    response: boolean;
  };
};

Background Script (src/background.ts)

import { createMessenger } from "@theluckystrike/webext-messaging";
import type { Messages } from "./types";

const msg = createMessenger<Messages>();

// Get the currently active tab
async function getActiveTab() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  if (!tab || !tab.id) return null;
  return {
    id: tab.id,
    title: tab.title || "",
    url: tab.url || "",
  };
}

// Handler for messages from popup
const unsubscribe = msg.onMessage({
  getActiveTab: async () => {
    return await getActiveTab();
  },
  getTabTitle: async (payload) => {
    const tab = await chrome.tabs.get(payload.tabId);
    return tab.title || "";
  },
  closeTab: async (payload) => {
    try {
      await chrome.tabs.remove(payload.tabId);
      return true;
    } catch {
      return false;
    }
  },
});

// Cleanup on uninstall
// unsubscribe();
import { createMessenger } from "@theluckystrike/webext-messaging";
import type { Messages } from "./types";

const msg = createMessenger<Messages>();

async function init() {
  // Get active tab info
  const tab = await msg.send("getActiveTab", undefined);
  
  if (tab) {
    console.log("Active tab:", tab.title, tab.url);
    
    // Get full title from background
    const fullTitle = await msg.send("getTabTitle", { tabId: tab.id });
    console.log("Full title:", fullTitle);
  }
  
  // Close button handler
  document.getElementById("close-btn")?.addEventListener("click", async () => {
    if (tab) {
      await msg.send("closeTab", { tabId: tab.id });
      window.close();
    }
  });
}

init();

Wire Format

Messages sent as Envelope: { type: string, payload: request }. Automatic — never construct manually.

Next Steps

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

Turn Your Extension Into a Business

Ready to monetize? The Extension Monetization Playbook covers freemium models, Stripe integration, subscription architecture, and growth strategies for Chrome extension developers.

No previous article
No next article