util.isDeepStrictEqual()

Compares two values for deep strict equality and returns a boolean instead of throwing.

Signature
util.isDeepStrictEqual(val1, val2[, options])
  • Added in Node.js v9.0.0

Parameters

ParameterTypeDescription
$val1anyThe first value to compare.
$val2anyThe second value to compare.
$options.skipPrototypeoptionalboolean= falseSkip prototype and constructor comparison, so objects with different classes can still compare equal on their enumerable properties. Added in v24.9.0.

Return value

booleantrue if there is deep strict equality between val1 and val2, otherwise false.

Errors

Does not throw for ordinary values; it returns false where assert.deepStrictEqual() would throw an AssertionError.

Changelog

  • PHP v24.9.0Added the options parameter with skipPrototype, allowing prototype and constructor comparison to be skipped.

util.isDeepStrictEqual() answers the question assert.deepStrictEqual() asks, but returns true/false instead of throwing an AssertionError. Same comparison algorithm, different control flow.

When to reach for it

Use it in ordinary program logic rather than in tests: deduplicating a list of objects, skipping a re-render or a write when state has not changed, deciding whether a cached value is still valid, comparing two parsed configuration files. In tests you usually still want assert.deepStrictEqual(), because its failure message shows a diff and this function only tells you no.

What the algorithm actually does

  • Strict means strict. 1 and '1' are not equal, and prototypes are compared — {} is not equal to Object.create(null), and two classes with identical fields are not equal. Node v24.9.0 added skipPrototype for the last case.
  • NaN equals NaN (SameValueZero), but +0 does not equal -0.
  • Key order is irrelevant, own enumerable symbol keys are compared, and Map/Set members are compared structurally regardless of insertion order.
  • Cycles are handled — self-referencing objects compare fine rather than recursing forever.
  • Functions compare by reference only, so two identical arrow functions are never equal.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Skip a database write or a file save when the new state is structurally identical to the last one.
  • Remove structurally duplicate objects from a list of parsed events.
  • Decide whether a memoised result is still valid for the arguments just supplied.

Frequently asked questions

What is the difference between util.isDeepStrictEqual and assert.deepStrictEqual?

They run the same comparison; only the outcome differs. `util.isDeepStrictEqual()` returns `true` or `false`, so it belongs in program logic, while `assert.deepStrictEqual()` throws an `AssertionError` carrying a rendered diff, which is what you want in a test.

Why do two class instances with the same properties compare as false?

Because deep strict equality includes prototypes and constructors, so a `Point` is never equal to a `Coord` or to a plain object, even with identical fields. From Node v24.9.0 pass `{ skipPrototype: true }` to compare enumerable properties only; on older versions, spread both values into plain objects first.

Does util.isDeepStrictEqual handle circular references?

Yes. It tracks values it has already visited, so two self-referencing objects with the same structure return `true` rather than overflowing the stack. It also handles `Map`, `Set`, typed arrays, `Date`, `RegExp` and symbol keys, all of which `JSON.stringify` comparison gets wrong.

More examples

Skipping work when state has not changed

This is the canonical use: a cheap structural check that avoids expensive I/O. Note `structuredClone(state)` when storing the baseline — keeping a reference to the caller's object would let later mutations make the comparison lie. The dedupe filter is O(n²) and fine for short lists; for large ones, key on a canonical string instead.

Example · javascript
const util = require('node:util');
const fs = require('node:fs');

let lastWritten = null;

function persist(state) {
  if (util.isDeepStrictEqual(state, lastWritten)) {
    console.log('unchanged, skipping write');
    return false;
  }
  fs.writeFileSync('state.json', JSON.stringify(state));
  lastWritten = structuredClone(state);
  console.log('wrote state.json');
  return true;
}

persist({ users: 3, tags: ['a', 'b'] }); // wrote state.json
persist({ users: 3, tags: ['a', 'b'] }); // unchanged, skipping write
persist({ users: 4, tags: ['a', 'b'] }); // wrote state.json

// Deduplicating a list of structurally equal objects:
const events = [{ t: 1 }, { t: 1 }, { t: 2 }];
const unique = events.filter(
  (e, i) => events.findIndex((o) => util.isDeepStrictEqual(o, e)) === i,
);
console.log(unique); // [ { t: 1 }, { t: 2 } ]

Where it differs from assert and from lodash isEqual

The prototype rule is the most common surprise for people arriving from lodash: `isEqual` compares own enumerable properties and calls a class instance equal to a plain object with the same fields, while `isDeepStrictEqual` does not. Before Node v24.9.0 the only workarounds were spreading both sides into plain objects or comparing field by field; `skipPrototype: true` now does it directly. Functions are compared by reference in both libraries.

Example · javascript
const util = require('node:util');
const assert = require('node:assert');

// Same algorithm, different control flow.
try {
  assert.deepStrictEqual({ a: 1 }, { a: 2 });
} catch (err) {
  console.log(err.code);    // ERR_ASSERTION
  console.log(err.message); // includes a rendered diff
}
console.log(util.isDeepStrictEqual({ a: 1 }, { a: 2 })); // false

// Prototypes are part of the comparison.
class Point { constructor(x) { this.x = x; } }
class Coord { constructor(x) { this.x = x; } }
console.log(util.isDeepStrictEqual(new Point(1), new Coord(1))); // false
console.log(util.isDeepStrictEqual(new Point(1), { x: 1 }));     // false
// lodash's isEqual returns true for both of those.

// Node v24.9.0 and later can opt out:
// console.log(util.isDeepStrictEqual(new Point(1), new Coord(1),
//   { skipPrototype: true })); // true

// Functions compare by identity, never structurally.
console.log(util.isDeepStrictEqual({ fn: () => 1 }, { fn: () => 1 })); // false
const fn = () => 1;
console.log(util.isDeepStrictEqual({ fn }, { fn }));                   // true

Built-in types, cycles and symbol keys

The last two lines are the pair worth remembering: two distinct `Symbol('k')` values are different symbols, so objects keyed by them are not equal, and a naive recursive comparison of `a` and `b` would never terminate while this one returns `true`. `Set` and `Map` comparison is order-insensitive, and typed arrays must match in both contents and constructor.

Example · javascript
const util = require('node:util');

// Dates, RegExps and errors compare by value.
console.log(util.isDeepStrictEqual(new Date(0), new Date(0)));   // true
console.log(util.isDeepStrictEqual(/a/gi, /a/gi));               // true
console.log(util.isDeepStrictEqual(/a/g, /a/i));                 // false

// Typed arrays compare element-wise, but the exact type matters.
console.log(util.isDeepStrictEqual(new Uint8Array([1]), new Uint8Array([1])));  // true
console.log(util.isDeepStrictEqual(new Uint8Array([1]), new Int8Array([1])));   // false

// Set and Map ignore insertion order.
console.log(util.isDeepStrictEqual(new Set([1, 2]), new Set([2, 1])));          // true

// Own enumerable symbol keys are compared.
const s = Symbol('k');
console.log(util.isDeepStrictEqual({ [s]: 1 }, { [s]: 1 }));                    // true
console.log(util.isDeepStrictEqual({ [Symbol('k')]: 1 }, { [Symbol('k')]: 1 })); // false

// Circular structures terminate instead of overflowing the stack.
const a = { name: 'x' }; a.self = a;
const b = { name: 'x' }; b.self = b;
console.log(util.isDeepStrictEqual(a, b));                                      // true

Discussion

  • Be the first to comment on this lesson.