denoland/deno · error · Error
plan expected ${this.#expected} assertion(s) but received ${
Error message
plan expected ${this.#expected} assertion(s) but received ${this.#actual} What it means
t.plan(count) in node:test registers a TestPlan with the expected number of assertions. The test context wraps its assert object so every t.assert.* call (and t.assert.fileSnapshot) increments a counter, and when the body finishes _checkPlan() compares actual against expected. Any mismatch — too few or too many — throws 'plan expected N assertion(s) but received M'.
Source
Thrown at ext/node/polyfills/testing.ts:1353
}
return undefined;
}
class TestPlan {
#expected;
#actual = 0;
constructor(count) {
this.#expected = count;
}
increment() {
this.#actual++;
}
check() {
if (this.#actual !== this.#expected) {
throw new Error(
`plan expected ${this.#expected} assertion(s) but received ${this.#actual}`,
);
}
}
}
class NodeTestContext {
#denoContext;
#afterHooks = [];
#beforeHooks = [];
#parent;
#skipped = false;
#name;
#abortController = new AbortController();
#plan;
#planAssert;
#beforeEachHooks = [];
#afterEachHooks = [];View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Make every planned assertion unconditional — move variability into assertion arguments, not around the assert calls
- Recount after editing: add/remove assertions and update t.plan(n) in the same change
- Verify you are calling t.assert.* (the context's wrapped object), not the imported node:assert — only the former is counted
- Derive the count from data when looping: t.plan(cases.length) then one assert per case
- If the exact count is unknowable, drop t.plan entirely — it is optional
Example fix
// before
test('sum', (t) => {
t.plan(3);
t.assert.equal(1 + 1, 2);
if (process.env.EXTRA) t.assert.ok(true); // skipped -> received 2
});
// after
test('sum', (t) => {
t.plan(2);
t.assert.equal(1 + 1, 2);
t.assert.ok(true);
}); Defensive patterns
Strategy: validation
Validate before calling
// dev-time dry run: count t.assert usage to derive the plan
function planFromBody(t, body) {
let count = 0;
const assert = new Proxy(t.assert, {
get(target, prop) {
const v = Reflect.get(target, prop);
return typeof v === 'function'
? (...args) => { count++; return v(...args); }
: v;
},
});
return { run: body({ ...t, assert }), count: () => count };
}
// use once locally, then hard-code t.plan(count) in the test Prevention
- Only assertions through t.assert.* are counted — never mix in the imported node:assert module inside a planned test
- Keep assertions unconditional; move variability into arguments, not branches
- When looping, derive the plan: t.plan(cases.length)
- Update t.plan in the same edit that adds or removes an assertion
When it happens
Trigger: t.plan(3) with only two assertions executed because an early return or if-branch skipped one; asserting more times than planned inside a loop; using the imported node:assert functions instead of t.assert.* so calls are never counted.
Common situations: Conditional assertions (if (debug) t.assert.ok(...)); loop-driven asserts whose iteration count varies with data; refactors that swap t.assert for the global assert module; forgetting that plan counts only this test's own asserts, not subtests.
Related errors
- ERR_INVALID_ARG_TYPE
- test was expected to fail but passed
- before() requires a function
- after() requires a function
- beforeEach() requires a function
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/82231297ba643658.
Report an issue: GitHub.