denoland/deno · error · Error

Unable to deserialize RecordableHistogram

Error message

Unable to deserialize RecordableHistogram

What it means

When a RecordableHistogram crosses an isolate boundary (worker.postMessage / structuredClone), Deno's clone registry (02_register_cloneable.js) re-attaches the native handle by clone id. If the target isolate's registry has no entry for that id, deserializeRecordableHistogram throws this plain Error — it has no error code, so it is easy to miss in code-based handling.

Source

Thrown at ext/node/polyfills/perf_hooks.js:606

      }
      return data.max;
    },
    percentileBigInt: (p) => {
      const entries = data.percentilesBigInt ?? [[100, "0"]];
      for (let i = 0; i < entries.length; i++) {
        if (entries[i][0] >= p) return BigInt(entries[i][1]);
      }
      return BigInt(data.maxBigInt);
    },
    reset() {},
  });
}

// Registered eagerly from `02_register_cloneable.js`; impl stays lazy here.
function deserializeRecordableHistogram(data) {
  const handle = MapPrototypeGet(histogramCloneRegistry, data.id);
  if (handle === undefined) {
    throw new Error("Unable to deserialize RecordableHistogram");
  }
  return new RecordableHistogram(handle, data.id);
}

function validateInteger(value, name, min, max) {
  if (typeof value === "bigint") {
    if (value < BigInt(min) || value > BigInt(max)) {
      throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value);
    }
    return Number(value);
  }
  if (typeof value !== "number" || !NumberIsInteger(value)) {
    throw new ERR_INVALID_ARG_TYPE(name, ["integer", "bigint"], value);
  }
  if (value < min || value > max) {
    throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value);
  }
  return value;

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Don't ship RecordableHistogram objects across isolates — create a fresh histogram inside each worker with createHistogram() and record locally
  2. When you need aggregates on the main thread, send plain numbers (percentiles, mean, count) and combine them there
  3. If you must move one, re-create the histogram at the destination and replay/merge values via record()
  4. Catch this un-coded Error by message when postMessage-adjacent code handles failures

Example fix

// before
import { parentPort } from "node:worker_threads";
parentPort.on("message", (h) => h.record(100n)); // may fail to deserialize

// after
import { parentPort } from "node:worker_threads";
import { createHistogram } from "node:perf_hooks";
const local = createHistogram();
parentPort.on("message", (n) => local.record(n));
Defensive patterns

Strategy: fallback

Validate before calling

// Before using anything received from another isolate:
const RecordableHistogram = require("node:perf_hooks").createHistogram().constructor;
const looksLikeHistogram = (v) => v instanceof RecordableHistogram;
// If false, treat it as plain data and rebuild rather than calling record/add on it

Type guard

const isUsableHistogram = (v) =>
  v instanceof require("node:perf_hooks").createHistogram().constructor;

Try / catch

try {
  received.record(n);
} catch (err) {
  if (err.message === "Unable to deserialize RecordableHistogram") {
    localHistogram.record(n); // fallback: keep a locally created histogram
  } else throw err;
}

Prevention

When it happens

Trigger: worker.postMessage(histogram) followed by calling record()/add() on it inside the worker; structuredClone(histogram) then using the result; returning a histogram from a worker to the main thread; using a histogram whose originating worker already exited.

Common situations: Collecting latency metrics from worker pools; Deno version skew or registration-order differences between the two isolates; libraries that transparently structured-clone arguments (NestedWorker, puppeteer-style helpers).

Related errors


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