jashkenas/underscore · error · RangeError

Comparison limit exceeded. Wrap call to isEqual in try/catch

Error message

Comparison limit exceeded. Wrap call to isEqual in try/catch or limit the depth of compared objects.

What it means

_.isEqual keeps a global count of comparisons during a single call; when recursive/cyclic data pushes that count past the internal comparisonLimit, the cycleTracker aborts the operation with a RangeError rather than degrading performance quadratically. It means your inputs are too deep, too large, or contain cycles — not that the values differ.

Source

Thrown at modules/isEqual.js:86

  return {
    tracked: [],
    trackedB: [],
    lookups: 0,
    push: function(a, b) {
      this.tracked.push(a);
      this.trackedB.push(b);
    },
    pop: function() {
      this.tracked.pop();
      this.trackedB.pop();
    },
    has: function(a) {
      // While the algorithm could run to arbitrary comparison depth in
      // principle, the quadratic runtime cost is going to hurt performance
      // significantly once the depth reaches over a few thousand levels. To
      // prevent excessive performance degradation, we keep track of the number
      // of comparisons and abort the operation when this number passes a limit.
      if (this.lookups >= comparisonLimit) throw RangeError(
        'Comparison limit exceeded. Wrap call to isEqual in try/catch or ' +
        'limit the depth of compared objects.'
      );
      // The following loop is **the** hot loop, so we keep it as light as
      // possible.
      for (var i = 0, l = this.tracked.length; i < l; ++i) {
        if (this.tracked[i] === a) break;
      }
      // We only update the number of comparisons after the loop, exploiting the
      // fact that `i` is still in scope and contains the approximate number of
      // comparisons made. `this.lookups` can exceed `comparisonLimit` as a
      // result, but this is unproblematic.
      this.lookups += i;
      // Return the index where `a` was found, plus one so it doesn't look
      // falsy.
      return i < l ? i + 1 : false;
    },
    // The `match` method takes one argument more than the corresponding method

View on GitHub (pinned to e70d5bd070)

Solutions

  1. Break cycles before comparing: strip or replace circular references (e.g. with a cycle-removal helper) and then call _.isEqual.
  2. Compare a shallow or bounded-depth projection instead: pick specific keys or cap the nesting level you care about.
  3. Use a diff/deep-equal library that supports cyclic data natively (e.g. one tracking visited nodes with WeakMap/WeakSet) for graph-shaped data.
  4. Compare canonical serializations (JSON.stringify with a cycle-safe replacer) when semantic identity is what matters.
  5. If the size is the problem, chunk the comparison: compare top-level keys/sections with separate _.isEqual calls.

Example fix

// before
_.isEqual(circularA, circularB);
// RangeError: Comparison limit exceeded...

// after
const strip = (v, seen = new WeakSet()) => {
  if (v && typeof v === 'object') {
    if (seen.has(v)) return '[circular]';
    seen.add(v);
    return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, strip(x, seen)]));
  }
  return v;
};
_.isEqual(strip(circularA), strip(circularB));
Defensive patterns

Strategy: try-catch

Validate before calling

function detectCycle(v, seen = new WeakSet()) {
  if (v && typeof v === 'object') {
    if (seen.has(v)) return true;
    seen.add(v);
    return Object.values(v).some((x) => detectCycle(x, seen));
  }
  return false;
}
// if (detectCycle(a) || detectCycle(b)) use a cycle-safe comparison instead

Type guard

const isCycleSafe = (v, seen = new WeakSet()) => {
  if (v === null || typeof v !== 'object') return true;
  if (seen.has(v)) return false;
  seen.add(v);
  return Object.values(v).every((x) => isCycleSafe(x, seen));
};

Try / catch

try {
  return _.isEqual(a, b);
} catch (e) {
  if (e instanceof RangeError && /Comparison limit exceeded/.test(e.message)) {
    return deepEqualCycleSafe(a, b); // fallback comparator using WeakSet of visited nodes
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling _.isEqual(a, b) where a/b are (a) circularly referenced objects or arrays, (b) extremely deeply nested structures (thousands of levels), or (c) very large sibling-heavy structures whose pairwise comparisons accumulate past the limit.

Common situations: Comparing parsed JSON that unexpectedly contains back-references (e.g. linked parent/child nodes or DOM-like trees); diffing Redux/ORM entity graphs with cycles; comparing two huge auto-generated config trees; deep-cloned structures that preserved circular links.


AI-assisted analysis of jashkenas/underscore@e70d5bd070 (2026-08-29). Data as JSON: /api/errors/432d744531d185d8. Report an issue: GitHub.