denoland/deno · error · TypeError

Snapshot serializer must return a string

Error message

Snapshot serializer must return a string

What it means

Thrown by assertSnapshot() (cli/js/40_test_snapshot.js) when the serializer applied to the actual value does not return a string. The default serializer (serialize) always yields a string; a custom options.serializer must too, because snapshot files store plain string entries keyed by test name and counter.

Source

Thrown at cli/js/40_test_snapshot.js:337

    throw new TypeError(
      "Expected the second argument to assertSnapshot() to be an options object or a message string",
    );
  }

  const context = getSnapshotContext(options);
  const testName = options.name ?? getFullTestName(tContext);
  const count = (MapPrototypeGet(context.counts, testName) ?? 0) + 1;
  MapPrototypeSet(context.counts, testName, count);
  const name = `${testName} ${count}`;

  if (!ArrayPrototypeIncludes(context.updateQueue, name)) {
    ArrayPrototypePush(context.updateQueue, name);
  }

  const serializer = options.serializer ?? serialize;
  const actualSnapshot = serializer(actual);
  if (typeof actualSnapshot !== "string") {
    throw new TypeError("Snapshot serializer must return a string");
  }

  const expectedSnapshot = MapPrototypeGet(context.currentValues, name);

  if (getIsUpdateMode()) {
    if (actualSnapshot !== expectedSnapshot) {
      MapPrototypeSet(context.updatedValues, name, actualSnapshot);
      if (!ArrayPrototypeIncludes(context.updatedNames, name)) {
        ArrayPrototypePush(context.updatedNames, name);
      }
    }
    return;
  }

  if (!context.fileExists) {
    throw new AssertionError(
      getErrorMessage("Missing snapshot file.", options),
    );

View on GitHub (pinned to 89f33cbef2)

Solutions

  1. Make the serializer total: coerce the final result with String(...) or JSON.stringify(...)
  2. Handle every branch — no early returns that drop the value
  3. Unit-test the serializer directly against the same shapes your snapshots capture

Example fix

// before
await t.assertSnapshot(data, {
  serializer: (v) => v.items.map((i) => i.name).join("\n") && v.items.length
    ? v.items.map((i) => i.name).join("\n")
    : undefined,
});

// after
await t.assertSnapshot(data, {
  serializer: (v) => v.items.map((i) => i.name).join("\n") ?? "",
});
Defensive patterns

Strategy: type-guard

Validate before calling

function makeSerializer(fn) {
  return (value) => {
    const out = fn(value);
    if (typeof out !== "string") {
      throw new TypeError(`serializer returned ${typeof out}; wrap with String()`);
    }
    return out;
  };
}
// use: t.assertSnapshot(v, { serializer: makeSerializer(fmt) })

Type guard

/** @param {(v: unknown) => unknown} fn */
function returnsString(fn, sample) {
  return typeof fn(sample) === "string";
}

Prevention

When it happens

Trigger: Passing { serializer: (v) => v } (identity on a non-string), a serializer returning undefined/null on some path (early return), or one returning an object/number like (v) => JSON.stringify(v, null, 2) missing the stringify. Fires per assertion, before any comparison.

Common situations: Writing a custom serializer that formats only the expected input shape and silently returns undefined for edge cases; reusing a formatter that returns numbers for numeric values; refactoring a serializer to return structured data instead of text.

Related errors


AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16). Data as JSON: /api/errors/cf0f6909ccaee95f. Report an issue: GitHub.