denoland/deno · error · Error

v8.startupSnapshot.setDeserializeMainFunction() can only be

Error message

v8.startupSnapshot.setDeserializeMainFunction() can only be called once.

What it means

v8.startupSnapshot.setDeserializeMainFunction(fn, data) registers the single main callback that runs when a startup snapshot is deserialized; the API contract allows exactly one. The polyfill tracks a module-level deserializeMainCalled flag and throws a plain Error on any second call, matching Node.

Source

Thrown at ext/node/polyfills/v8.ts:588

// is an API-surface polyfill that lets modules calling `v8.startupSnapshot`
// load without errors. `isBuildingSnapshot()` always returns false. The
// serialize/deserialize callbacks are stored but never invoked because there
// is no snapshot lifecycle. `setDeserializeMainFunction` invokes the callback
// synchronously so scripts that register a deserialize main still run their
// entry point in plain Deno runs.
// deno-lint-ignore no-explicit-any
type SnapshotCallback = (data: any) => unknown;
const serializeCallbacks: { fn: SnapshotCallback; data: unknown }[] = [];
const deserializeCallbacks: { fn: SnapshotCallback; data: unknown }[] = [];
let deserializeMainCalled = false;

function startupSnapshotSetDeserializeMainFunction(
  fn: SnapshotCallback,
  data?: unknown,
) {
  validateFunction(fn, "callback");
  if (deserializeMainCalled) {
    throw new Error(
      "v8.startupSnapshot.setDeserializeMainFunction() can only be called once.",
    );
  }
  deserializeMainCalled = true;
  fn(data);
}

function startupSnapshotAddSerializeCallback(
  fn: SnapshotCallback,
  data?: unknown,
) {
  validateFunction(fn, "callback");
  ArrayPrototypePush(serializeCallbacks, { fn, data });
}

function startupSnapshotAddDeserializeCallback(
  fn: SnapshotCallback,
  data?: unknown,

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Register the main function in exactly one module and delete the duplicate call.
  2. For repeatable setup use addSerializeCallback/addDeserializeCallback, which accept multiple registrations.
  3. Guard registration behind a module-level flag when re-execution is possible (HMR, tests).

Example fix

// before — two entrypoints each register a main
startupSnapshot.setDeserializeMainFunction(mainA);
startupSnapshot.setDeserializeMainFunction(mainB); // Error: can only be called once

// after — one main that dispatches, plus callbacks for the rest
startupSnapshot.setDeserializeMainFunction(mainA);
startupSnapshot.addDeserializeCallback(() => mainB());
Defensive patterns

Strategy: validation

Validate before calling

let mainRegistered = false;
export function registerSnapshotMain(fn, data) {
  if (mainRegistered) return; // idempotent across HMR/test re-imports
  mainRegistered = true;
  v8.startupSnapshot.setDeserializeMainFunction(fn, data);
}

Prevention

When it happens

Trigger: Calling setDeserializeMainFunction twice in the same isolate — module top-level registration re-executed during dev reload/HMR, or two modules both trying to own the snapshot entry point.

Common situations: Snapshot-building scripts refactored so registration moved into a shared helper invoked from two files; test harnesses importing the snapshot script twice; boilerplate duplicated between entry points.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/16a77847df0d4332. Report an issue: GitHub.