denoland/deno · error · AssertionError
ERR_ASSERTION
ERR_ASSERTION
Error message
Functions were not called the expected number of times
What it means
tracker.verify() runs report(), which compares each tracked function's actual call count against its expected count. Any mismatch throws AssertionError: with exactly one failure the message is that failure's own message; with two or more it is the aggregate 'Functions were not called the expected number of times', and err.details carries every mismatch (including expected/actual counts and the registered stack). This is the intended failure mode of a behavioral assertion, not an infrastructure error.
Source
Thrown at ext/node/polyfills/internal/assert/calltracker.js:160
const errors = [];
for (const context of new SafeSetIterator(this.#callChecks)) {
const message = context.report();
if (message !== undefined) {
ArrayPrototypePush(errors, message);
}
}
return errors;
}
verify() {
const errors = this.report();
if (errors.length === 0) {
return;
}
const message = errors.length === 1
? errors[0].message
: "Functions were not called the expected number of times";
throw new AssertionError({
message,
details: errors,
});
}
}
return {
CallTracker,
};
})();
View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Read err.details (or call tracker.report()) to see which function mismatched, with expected vs actual counts and the registration stack.
- Make the code under test call the wrapper tracker.calls() returned — reassign fn = tracker.calls(fn) so the real path is intercepted.
- Await all async operations before verify(), and pass the correct expected count for legitimately-variable call patterns.
- Use tracker.report() when you want failures as data without throwing.
Example fix
// before const save = tracker.calls(repo.save, 1); // wrapper never installed await api.createUser(input); // repo.save called on original path tracker.verify(); // AssertionError // after repo.save = tracker.calls(repo.save, 1); // intercept the real path await api.createUser(input); tracker.verify(); // passes
Defensive patterns
Strategy: validation
Validate before calling
const failures = tracker.report();
if (failures.length > 0) {
for (const f of failures) console.error(f.message, 'actual:', f.actual, 'expected:', f.expected);
} else {
tracker.verify();
} Try / catch
try {
tracker.verify();
} catch (err) {
if (err.code === 'ERR_ASSERTION' && Array.isArray(err.details)) {
for (const d of err.details) console.error(d.message);
} else throw err;
} Prevention
- Intercept the real code path: fn = tracker.calls(fn) or mock.method(obj, 'name').
- Await every async operation before verify().
- Call report() first when diagnosing flaky suites — it never throws.
- CallTracker is deprecated; migrate to node:test mock.fn + mock assertions for long-term support.
When it happens
Trigger: A wrapper created with tracker.calls(fn, 2) invoked 0, 1, or 3 times by the time verify() runs; async work not awaited before verify(); the code under test calling the original fn instead of the wrapper returned by calls().
Common situations: Event handlers not firing (wrong event name, listener attached after emit); missing await on promises before verification; stale expected counts after refactors.
Related errors
- ERR_INVALID_ARG_VALUE
- ERR_UNAVAILABLE_DURING_EXIT
- Snapshot does not match:\n\n ${green("[Diff]")} ${green("
- ERR_INVALID_OBJECT_DEFINE_PROPERTY
- ERR_ASSERTION
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/53870b29e35e9e9b.
Report an issue: GitHub.