assert.fail()
Throws an AssertionError unconditionally, with your message or the default 'Failed'.
assert.fail([message])- Added in Node.js v0.1.21
Parameters
| Parameter | Type | Description |
|---|---|---|
$messageoptional | string | Error= 'Failed' | The message for the AssertionError, or an Error instance which is thrown in its place. |
Errors
Always throws. Raises an AssertionError with code ERR_ASSERTION carrying message, or throws message directly when it is an Error instance.
Changelog
- PHP v10.0.0Calling assert.fail() with more than one argument is runtime-deprecated as DEP0094; use assert.strictEqual() or pass only a message.
assert.fail() never returns — it exists to mark a place the program should not have reached. The two situations that call for it are the default case of a switch that is meant to be exhaustive, and the line after a call you expected to throw.
The classic pattern
When you cannot use assert.throws() — because you want to run several assertions on the caught error — the shape is:
try {
risky();
assert.fail('risky() should have thrown');
} catch (err) {
if (err.code === 'ERR_ASSERTION') throw err;
assert.strictEqual(err.code, 'ERR_BAD_INPUT');
}
The re-throw guard matters: without it, the AssertionError that fail() just raised lands in your own catch and is inspected as though it were the error under test, so the test passes when the code did not throw. Most of the time assert.throws() with a validation function is the cleaner option and avoids the problem entirely.
Only one argument
The legacy five-argument form assert.fail(actual, expected, message, operator) still runs but emits the DEP0094 deprecation warning and no longer appears in the documentation. Pass a message, or nothing.
Example
When to use it
- Mark the default branch of a switch that is supposed to cover every enum value
- Fail a test explicitly when a call that should have thrown returned normally
- Stub out an unimplemented code path so reaching it is loud rather than silent
Frequently asked questions
How do I make a Node test fail on purpose?
Call assert.fail('reason') — it always throws an AssertionError with code ERR_ASSERTION carrying your message, or the default message 'Failed' if you pass nothing. Passing an Error instance throws that error instead.
Why does my assert.fail() inside a try block not fail the test?
Because the AssertionError it throws is caught by your own catch block and treated like the error you were testing for. Re-throw when err.code === 'ERR_ASSERTION', or replace the whole construct with assert.throws().
Is assert.fail(actual, expected, message, operator) still supported?
The multi-argument form still runs but emits the DEP0094 deprecation warning and has been removed from the documentation. Use assert.strictEqual() for a comparison, or pass assert.fail() a single message.
More examples
The try/catch pattern, done safely
Without the ERR_ASSERTION guard, the AssertionError raised by fail() would fall into the same catch block and be compared against ERR_CONFIG, turning a genuine failure into a confusing mismatch. The second half shows why assert.throws is usually the better tool: no guard, no control flow to get wrong.
const assert = require('node:assert');
function parseConfig(text) {
const cfg = JSON.parse(text);
if (!cfg.port) throw Object.assign(new Error('port is required'), { code: 'ERR_CONFIG' });
return cfg;
}
// Correct: re-throw the AssertionError so a non-throwing call really fails.
try {
parseConfig('{"host":"localhost"}');
assert.fail('parseConfig should have thrown for a missing port');
} catch (err) {
if (err.code === 'ERR_ASSERTION') throw err;
assert.strictEqual(err.code, 'ERR_CONFIG');
assert.match(err.message, /port is required/);
console.log('caught the expected error');
}
// Cleaner: let assert.throws own the control flow.
assert.throws(
() => parseConfig('{"host":"localhost"}'),
{ code: 'ERR_CONFIG', message: 'port is required' },
);
console.log('assert.throws form passed');An exhaustive-switch guard
Adding a new member to STATES without updating the switch surfaces immediately at the point of use, naming the value that was missed. This is the runtime counterpart of an exhaustiveness check: fail() never returns, so the function has no implicit undefined path.
const assert = require('node:assert');
const STATES = ['queued', 'running', 'done', 'failed'];
function icon(state) {
switch (state) {
case 'queued': return '.';
case 'running': return '>';
case 'done': return '+';
// 'failed' was never handled.
default: assert.fail(`unhandled state: ${state}`);
}
}
for (const state of STATES) {
try {
console.log(state, '->', icon(state));
} catch (err) {
console.log(state, '->', err.message);
}
}
// queued -> .
// running -> >
// done -> +
// failed -> unhandled state: failedInside the built-in test runner
The fail() call turns a runaway pagination loop into an immediate, well-named test failure instead of an infinite loop or an out-of-range read. It sits in the stub rather than in the assertions, which is where fail() is genuinely better than a comparison — there is nothing to compare, only a line that must never execute.
import test from 'node:test';
import assert from 'node:assert/strict';
async function* pages(fetchPage) {
let cursor = null;
do {
const page = await fetchPage(cursor);
yield page.items;
cursor = page.next;
} while (cursor);
}
test('pages stops at the last cursor', async () => {
const responses = [
{ items: [1, 2], next: 'c1' },
{ items: [3], next: null },
];
let i = 0;
const fetchPage = async () => {
if (i >= responses.length) assert.fail('pages() requested more data than expected');
return responses[i++];
};
const seen = [];
for await (const items of pages(fetchPage)) seen.push(...items);
assert.deepStrictEqual(seen, [1, 2, 3]);
});
// $ node --test
// ✓ pages stops at the last cursor (1.3ms)
Discussion