util.isDeepStrictEqual()
Compares two values for deep strict equality and returns a boolean instead of throwing.
util.isDeepStrictEqual(val1, val2[, options])- Added in Node.js v9.0.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$val1 | any | The first value to compare. |
$val2 | any | The second value to compare. |
$options.skipPrototypeoptional | boolean= false | Skip 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.
1and'1'are not equal, and prototypes are compared —{}is not equal toObject.create(null), and two classes with identical fields are not equal. Node v24.9.0 addedskipPrototypefor the last case. NaNequalsNaN(SameValueZero), but+0does not equal-0.- Key order is irrelevant, own enumerable symbol keys are compared, and
Map/Setmembers 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
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.
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.
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 })); // trueBuilt-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.
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