Inspecting Worker Scopes in Firefox DevTools

When a dedicated Web Worker is paused at a breakpoint, the Firefox Debugger’s Scopes pane gives complete visibility into its lexical environment — local variables, closure captures, module-level bindings, and the worker global itself. This page is part of the Firefox Worker Debugging guide, which covers the broader debugging workflow including Debugging, Profiling & Production Optimization.

Minimal Reproducible Example

The following worker and main-thread pair is the smallest complete example for demonstrating scope inspection. It deliberately includes multiple scope layers — a parameter, a module-level variable, and a closure — so every Scopes pane section is populated.

// worker.ts
let processedCount = 0; // module-level variable (appears in Worker scope)

function applyMultiplier(value: number, factor: number): number {
  const scaled = value * factor; // local variable (appears in Local scope)
  return scaled;
}

self.onmessage = ({ data }: MessageEvent<{ value: number; factor: number }>) => {
  const result = applyMultiplier(data.value, data.factor); // set breakpoint here
  processedCount++;
  self.postMessage({ result, processedCount });
};
// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });

worker.onmessage = ({ data }: MessageEvent<{ result: number; processedCount: number }>) => {
  console.log(`Result: ${data.result}, total processed: ${data.processedCount}`);
};

// Trigger the worker to pause at the breakpoint
worker.postMessage({ value: 42, factor: 3 });

Walkthrough: Reading the Scopes Pane

Step 1 — Open DevTools and set the breakpoint

Open DevTools before the page runs. Go to the Debugger panel, click on worker.ts under the Workers heading in the Sources list, and click line 7 (the const result = applyMultiplier(...) line) to set a blue breakpoint marker.

Step 2 — Trigger the breakpoint

Click the button or call triggerWorker() in the Console to fire worker.postMessage(...). Execution pauses inside the worker, the Debugger switches context to the worker thread, and the Scopes pane on the right fills in.

Step 3 — Read scope layers

The pane shows these sections from innermost to outermost:

Block — Any let or const declared in the current block scope but not yet initialized will appear here as <uninitialized>. In the example, result has not been assigned yet at the breakpoint (the right-hand side is still being computed), so it shows <uninitialized>.

Local — The function’s parameters and var declarations. For onmessage, the destructured data parameter shows {value: 42, factor: 3} as an expandable object. Click the triangle to see value: 42 and factor: 3 inline.

Closure — If onmessage were defined inside another function that captured variables, those would appear here. In the minimal example, this section is absent because onmessage is defined at module top level with no enclosing function.

Module — For a module worker, the module-level bindings are visible here: processedCount: 0, applyMultiplier: function. These are the worker’s equivalent of global variables but scoped to the module.

Worker — The worker’s global object (DedicatedWorkerGlobalScope). Expand it to see self, postMessage, onmessage, importScripts (classic workers), caches, indexedDB, setTimeout, and any globally-defined variables.

Step 4 — Step into applyMultiplier

Press F11 to step into the applyMultiplier call. The Scopes pane now shows the inner function’s scope:

  • Local: value: 42, factor: 3
  • Block: scaled: <uninitialized> (not yet assigned)

Press F10 (Step Over) to execute const scaled = value * factor. The Block scope updates: scaled: 126.

Step 5 — Use the console context picker to evaluate expressions

Switch to the Console panel. Click the context picker dropdown — it shows “Top” by default. Select the worker thread (labeled with its script URL or worker.ts).

Now type in the Console:

processedCount        // → 0  (module-level, read before increment)
typeof self           // → "object"
self.constructor.name // → "DedicatedWorkerGlobalScope"
applyMultiplier(10, 5) // → 50  (call a function defined in the worker)

The last expression calls the worker’s own function and returns its result directly in the console — no round-trip through postMessage needed. This is powerful for testing pure functions in the worker’s actual environment.

Step 6 — Add watch expressions

In the Debugger’s right sidebar, find the Watch Expressions section (sometimes labeled Expressions). Click the + button and type processedCount. This expression re-evaluates every time execution pauses, showing the current value without needing to expand the Module scope manually.

Add a second watch: data.value * data.factor. This evaluates the multiplication in-scope and shows 126 while paused at the breakpoint.

Gotchas and Edge Cases

Source maps for bundled workers

When you bundle a worker with Vite or webpack, the Debugger shows the original TypeScript source only if the source map is valid and accessible. A common failure: the source map references ../../src/worker.ts but the file server does not serve files outside the output directory. Verify by looking at the Sources list — if the worker entry shows .js (the compiled bundle) instead of .ts, the source map is not loading. Set devtool: 'source-map' (webpack) or build.sourcemap: true (Vite) and confirm the .map file is served with Content-Type: application/json.

Worker must be running to appear

If you open DevTools after the page has already created and terminated a worker, the worker will not appear in the Sources list. This is because Firefox registers threads dynamically as they start. To debug a short-lived worker, either:

  • Add a setTimeout(() => {}, 60000) call inside the worker to keep it alive during debugging, or
  • Set the breakpoint before the worker finishes: use the Event Listener Breakpoints → Worker → message setting so Firefox pauses the worker as soon as it receives the first message, before it can complete and terminate.

Blob worker URLs in the Scopes pane

Workers created from a Blob URL have a blob:https://... source URL. In the Scopes pane, the Worker global section may show this URL instead of a human-readable name. Add a //# sourceURL=my-worker.js comment as the first line inside the blob string — Firefox uses this as the display label both in the Sources list and in the Scopes pane’s thread identifier.

// Annotate blob workers for the Scopes pane
const src = `
  //# sourceURL=data-processor.js
  let state = 'ready';
  self.onmessage = ({ data }) => {
    state = 'processing';
    self.postMessage(data.value * 2);
    state = 'ready';
  };
`;
const worker = new Worker(URL.createObjectURL(new Blob([src], { type: 'application/javascript' })));

Module workers and top-level await

If the worker module uses top-level await (e.g., const wasm = await WebAssembly.instantiateStreaming(...)) and the await is still in flight when a breakpoint fires, the Module scope section may show bindings from before the await settled. The variables initialized after the awaited expression will appear as <uninitialized>. Resume past the await point to see them fully initialized.

Concrete Performance Rule of Thumb

Reading scope variables from the Scopes pane is a zero-cost DevTools operation — it reads the current V8 heap state, no script executes. However, evaluating expressions in the console context does execute code in the worker’s heap. Evaluating a function that triggers GC or allocates large arrays will affect subsequent profiler measurements for that session. Keep console evaluations to simple property reads and pure function calls when you need accurate profiling data.

For runtime inspection without pausing, use performance.measure inside the worker and read the entries via performance.getEntriesByType('measure') in the console context — this gives cumulative timing data without stopping execution.

Frequently Asked Questions

Why is the Scopes pane empty when I pause inside a worker?
The Scopes pane only populates when execution is paused at a valid source location. If the breakpoint is on a compiled bundle without a source map, Firefox may not be able to resolve scope metadata. Add a source map and re-set the breakpoint. Also confirm the worker was created after DevTools opened — workers that pre-date the DevTools session may lack full source registration.
Can I modify a variable's value from the Scopes pane while paused?
Yes. Double-click any primitive value in the Scopes pane to enter edit mode. Changes take effect when you resume execution. This is useful for testing edge-case inputs without rewriting the worker script — for example, changing a loop counter or a flag value mid-execution.

See also