JavaScript Basics: Choose Precise Strict Assertions
A test is only as useful as the claim its assertion actually makes. Identity, nested structure, and rejected promises need different checks. This tutorial examines those distinctions directly, including cases that can look equal when printed but should fail a particular assertion.
These independent programs were tested on Linux with Node.js v24.19.0. They use the built-in node:assert/strict module without a test runner or external packages. Deliberately failing comparisons are captured by outer assertions, so all three completed examples exit successfully.
Prepare a scratch directory
You need Bash, a text editor, Node.js 24 on your PATH, and the mktemp, rm, and rmdir utilities. Keep the same terminal open. Save each named source file as UTF-8 in this directory; .mjs selects ES modules. The setup stops if creating or entering the scratch directory fails.
node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1The tested runtime prints:
v24.19.01. Check exact values, including numeric edge cases
Save this as assert-exact-values.mjs:
import assert from 'node:assert/strict';
assert.equal(7, 7);
assert.equal(NaN, NaN);
assert.throws(() => assert.equal(7, '7'), assert.AssertionError);
assert.throws(() => assert.equal(0, -0), assert.AssertionError);
const record = { ready: true };
assert.equal(record, record);
assert.throws(() => assert.equal(record, { ready: true }), assert.AssertionError);
console.log('Numeric strings stay distinct from numbers');
console.log('NaN matches itself; positive and negative zero differ');
console.log('Object identity requires the same reference');Run it:
node assert-exact-values.mjsOutput:
Numeric strings stay distinct from numbers
NaN matches itself; positive and negative zero differ
Object identity requires the same referenceWith this strict import, equal() uses the Object.is() comparison rule. That makes NaN equal to itself and distinguishes positive zero from negative zero. It does not coerce a numeric string into a number. Object comparison checks whether both arguments identify the same object.
The nested assert.throws() calls prove that the unwanted comparisons fail with an AssertionError. In an ordinary test, write the assertion directly and let an unexpected failure stop the test. Catching every assertion would hide failures. See Node’s strict equality documentation.
2. Compare structure without erasing missing values
Save this as assert-nested-structure.mjs:
import assert from 'node:assert/strict';
const actual = { names: ['Ada', 'Lin'], options: { retries: 0 } };
const expected = { names: ['Ada', 'Lin'], options: { retries: 0 } };
assert.deepEqual(actual, expected);
assert.throws(() => assert.deepEqual({}, { result: undefined }), assert.AssertionError);
assert.throws(() => assert.deepEqual([,], [undefined]), assert.AssertionError);
assert.equal(JSON.stringify({}), JSON.stringify({ result: undefined }));
assert.equal(JSON.stringify([,]), JSON.stringify([undefined]));
console.log('Separate objects with matching nested data pass');
console.log('Missing properties and sparse slots remain distinct');
console.log('JSON comparison loses these distinctions');Run it:
node assert-nested-structure.mjsOutput:
Separate objects with matching nested data pass
Missing properties and sparse slots remain distinct
JSON comparison loses these distinctionsdeepEqual() from the strict module compares these nested arrays and plain objects structurally. A missing property differs from a present property containing undefined. Likewise, an empty array slot differs from an explicitly stored undefined element.
The JSON checks show why serializing both sides is a poor replacement: it drops the undefined-valued object property and turns both array cases into [null]. Here that would erase exactly the differences the test should catch. Compare the data returned by the function, not a convenient display format. More complex objects have additional comparison rules, so consult the deep strict comparison details when moving beyond plain data.
3. Await and match the intended rejection
Save this as assert-promise-rejection.mjs:
import assert from 'node:assert/strict';
async function readSetting(name) {
if (name !== 'theme') {
const error = new Error(`Unknown setting: ${name}`);
error.code = 'UNKNOWN_SETTING';
throw error;
}
return 'dark';
}
assert.equal(await readSetting('theme'), 'dark');
await assert.rejects(() => readSetting('missing'), {
name: 'Error', code: 'UNKNOWN_SETTING', message: 'Unknown setting: missing',
});
await assert.rejects(
() => assert.rejects(() => readSetting('theme')),
assert.AssertionError,
);
console.log('Known setting: dark');
console.log('Unknown setting rejects with the expected details');
console.log('A fulfilled promise cannot satisfy a rejection assertion');Run it:
node assert-promise-rejection.mjsOutput:
Known setting: dark
Unknown setting rejects with the expected details
A fulfilled promise cannot satisfy a rejection assertionassert.rejects() returns a promise, so await it. The callback lets the assertion start and observe the operation. Matching the code and message prevents an unrelated error from counting as the intended failure. The final nested check verifies that an unexpectedly fulfilled promise fails the rejection assertion.
Use assert.throws() for a synchronous throw and assert.rejects() for a rejected promise. An async function turns its thrown error into a rejection, even before its first await. This example has no network or timer, keeping the behavior deterministic. See Node’s rejects documentation. Choose expected values from the function’s intended contract, not merely from whatever the current implementation happens to print.
Clean up
In the same terminal, remove only the named files, return to the original directory, and remove the empty scratch directory:
rm -- assert-exact-values.mjs assert-nested-structure.mjs assert-promise-rejection.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.