jsguides

assert

assert.ok(value[, message])

Importing assert

The node:assert module is built into Node and needs no install. Three import patterns are available:

// legacy mode (not recommended for new code)
import assert from 'node:assert';

// strict mode (recommended)
import { strict as assert } from 'node:assert';
// or, equivalently since v15.0.0:
import assert from 'node:assert/strict';

Use node:assert/strict for new code. Legacy mode’s loose == produces surprises, and every recent Node release recommends the strict surface.

Strict and legacy mode

Only four methods behave differently between the two modes:

MethodLegacy (default)Strict
assert.equal==alias of assert.strictEqual
assert.notEqual!=alias of assert.notStrictEqual
assert.deepEqualrecursive ==alias of assert.deepStrictEqual
assert.notDeepEqualrecursive !=alias of assert.notDeepStrictEqual

The legacy == semantics create footguns. The canonical example passes silently in legacy mode but throws in strict mode:

// legacy mode
import assert from 'node:assert';

assert.deepEqual('+00000000', false);  // OK ('' == false under loose coercion)
assert.equal(1, '1');                  // OK
assert.equal(NaN, NaN);                // OK (specially handled)

Re-run the same calls in strict mode and every one of them throws at the assertion boundary. Loose equality, the NaN self-equality special case, and the recursive string-to-boolean coercion that legacy mode permits are all gone the moment the import is switched to node:assert/strict:

// strict mode
import { strict as assert } from 'node:assert';

assert.deepEqual('+00000000', false);  // AssertionError
assert.equal(1, '1');                  // AssertionError

In strict mode assert.strictEqual uses Object.is(). That gives NaN === NaN but keeps +0 and -0 distinct, which is the opposite of === for the signed zeros. Default to strict.

AssertionError

Every failed assertion throws an assert.AssertionError, a subclass of Error. The constructor accepts an options object:

new assert.AssertionError({
  message: 'inputs differ',
  actual: 1,
  expected: 2,
  operator: 'strictEqual',
  stackStartFn: myFn, // omit earlier frames from the stack
  diff: 'full',       // 'simple' (default) or 'full'
});

Instances expose actual, expected, operator, and generatedMessage. The code property is always 'ERR_ASSERTION'. Standard Error.message and Error.name are also present. Catch the error with instanceof assert.AssertionError or by checking err.code === 'ERR_ASSERTION'.

Assertion methods

assert.ok(value[, message])

Throws if value is falsy. Added in v0.1.21. Since v10.0.0, calling it with no args throws No value argument passed to assert.ok(). The shorter assert(value) is the same function.

import { strict as assert } from 'node:assert';

assert.ok(2 + 2 === 4); // OK
assert.ok(0);           // AssertionError

assert.strictEqual and assert.equal

assert.strictEqual(actual, expected[, message]) compares with Object.is(). The loose assert.equal uses == in legacy mode and only matches the strict version when you import from node:assert/strict.

import { strict as assert } from 'node:assert';

assert.strictEqual(NaN, NaN); // OK (Object.is)
assert.strictEqual(0, -0);    // AssertionError

assert.deepStrictEqual and assert.deepEqual

Compares own-enumerable properties. The strict form uses Object.is() for primitives (so 0 and -0 differ), checks symbol-keyed properties, and compares prototypes. The loose form uses recursive ==. WeakMap, WeakSet, and Promise are always compared by reference, even in deep strict mode. This is deliberate: two WeakSets holding the same items are not deep-equal.

import { strict as assert } from 'node:assert';

assert.deepStrictEqual({ a: 1 }, { a: 1 });   // OK
assert.deepStrictEqual({ a: 1 }, { a: '1' }); // AssertionError

assert.partialDeepStrictEqual(actual, expected[, message])

Stabilized in v24.0.0. The expected only needs to specify a subset of properties; extra properties on actual are ignored. Useful when you care about a few fields and don’t want to list every one of them.

import { strict as assert } from 'node:assert';

assert.partialDeepStrictEqual(
  { a: 1, b: 2, c: 3 },
  { b: 2 },
); // OK

assert.match and assert.doesNotMatch

Added in v13.6.0, no longer experimental since v16.0.0. The first argument must be a string or the call throws ERR_ASSERTION. Use match to assert a string contains a pattern, and doesNotMatch for the negative case. Both fit well into testing flows and input-validation guards where you only care about a substring of a larger message.

import { strict as assert } from 'node:assert';

assert.match('hello world', /world/);  // OK
assert.doesNotMatch('hello', /world/); // OK

assert.throws and assert.rejects

assert.throws(fn[, error][, message]) runs fn and asserts it throws. The optional error argument can be a class (checked with instanceof), a RegExp (matched against String(err), which includes name), a validator function returning true, or an object whose properties must deep-strict-equal the thrown error. A string in this slot is treated as message, not a matcher, and triggers ERR_AMBIGUOUS_ARGUMENT when the thrown message happens to match the string.

assert.rejects(asyncFn[, error][, message]) is the async twin and returns a Promise. asyncFn can be a function or a Promise directly, so you can pass a rejected promise without an IIFE wrapper.

import { strict as assert } from 'node:assert';

assert.throws(
  () => {
    throw new TypeError('Wrong value');
  },
  { name: 'TypeError', message: /Wrong/ },
);

await assert.rejects(Promise.reject(new Error('boom')), /boom/);

assert.fail and assert.ifError

assert.fail([message]) throws an AssertionError with the default message 'Failed'. Passing more than one argument to assert.fail() is deprecated.

assert.ifError(value) throws if value is anything other than undefined or null. It is designed for the err argument in Node-style callbacks, so you can write assert.ifError(err); and have it throw the moment a callback receives a real error.

Custom Assert instances

new assert.Assert({ diff: 'full', strict: true, skipPrototype: false }), added in v24.6.0 (and v22.19.0), returns an isolated assertion object with its own options. Use it when you want 'full' diffs or skipPrototype behavior without flipping the entire module to strict mode. Note the version requirement: Node 18 LTS does not have this class.

A subtle gotcha: destructuring methods off an Assert instance drops the configuration. Methods become unbound and revert to default behavior (diff: 'simple', non-strict). Always call methods on the instance directly.

import { Assert } from 'node:assert';

const assertInstance = new Assert({ diff: 'full' });
assertInstance.deepStrictEqual({ a: 1 }, { a: 2 });
// Multi-line diff is enabled by 'full'

Message parameter

Every method that accepts a message accepts three forms:

  1. A string, used as-is. Extra arguments after the message go through util.format() and are substituted with printf-like tokens (%s, %d, %i, %f, %j, %o, %O, %%).
  2. An Error, thrown directly. If you also pass extra args, Node throws ERR_AMBIGUOUS_ARGUMENT.
  3. A Function of the form (actual, expected) => string, called only on failure. A non-string return falls back to the default message. Extra args plus a function also raise ERR_AMBIGUOUS_ARGUMENT.

Anything else raises ERR_INVALID_ARG_TYPE.

import { strict as assert } from 'node:assert';

const got = 1;
const want = 2;
assert.strictEqual(got, want, 'got %d, want %d', got, want);
// AssertionError [ERR_ASSERTION]: got 1, want 2

assert.strictEqual(1, 2, new TypeError('Inputs are not identical'));
// TypeError: Inputs are not identical

Disable diff colors with NO_COLOR=1 or NODE_DISABLE_COLORS=1 when running in a terminal that mishandles ANSI escapes.

Common patterns

The module is a Node.js builtin, and the strict surface fits naturally into both runtime argument checks and unit testing in plain JavaScript. A few patterns come up often.

Argument validation

For argument checks at the top of a function, throw early and skip the rest of the body. Every function that takes untrusted input can use the same set of checks to fail loudly with structured errors, which is usually easier to debug than letting a TypeError surface from three frames down the call stack.

import { strict as assert } from 'node:assert';

function divide(numerator, denominator) {
  assert.ok(Number.isFinite(numerator), 'numerator must be finite');
  assert.strictEqual(typeof denominator, 'number', 'denominator must be a number');
  assert.notStrictEqual(denominator, 0, 'denominator must not be zero');
  return numerator / denominator;
}

Testing code

In testing code, assert is the foundation that frameworks like node:test and Vitest layer their own matchers on top of. The same calls work without a runner when you need a quick check in a script.

import { strict as assert } from 'node:assert';
import { test } from 'node:test';

test('divide handles non-zero denominators', async () => {
  assert.strictEqual(divide(10, 2), 5);
  await assert.rejects(() => divide(1, 0), /zero/);
});

Snapshots and partial matches

For state-machine or builder-style code, assert.deepStrictEqual on a snapshot of the object after a sequence of operations catches accidental shape changes that property-by-property checks would miss. assert.partialDeepStrictEqual works when you only care about a few of the fields and the rest can drift.

See also