{"record":{"id":"432d744531d185d8","repo":"jashkenas/underscore","slug":"comparison-limit-exceeded-wrap-call-to-isequal-in","errorCode":null,"errorMessage":"Comparison limit exceeded. Wrap call to isEqual in try/catch or limit the depth of compared objects.","messagePattern":"Comparison limit exceeded\\. Wrap call to isEqual in try/catch or limit the depth of compared objects\\.","errorType":"exception","errorClass":"RangeError","httpStatus":null,"severity":"error","filePath":"modules/isEqual.js","lineNumber":86,"sourceCode":"  return {\n    tracked: [],\n    trackedB: [],\n    lookups: 0,\n    push: function(a, b) {\n      this.tracked.push(a);\n      this.trackedB.push(b);\n    },\n    pop: function() {\n      this.tracked.pop();\n      this.trackedB.pop();\n    },\n    has: function(a) {\n      // While the algorithm could run to arbitrary comparison depth in\n      // principle, the quadratic runtime cost is going to hurt performance\n      // significantly once the depth reaches over a few thousand levels. To\n      // prevent excessive performance degradation, we keep track of the number\n      // of comparisons and abort the operation when this number passes a limit.\n      if (this.lookups >= comparisonLimit) throw RangeError(\n        'Comparison limit exceeded. Wrap call to isEqual in try/catch or ' +\n        'limit the depth of compared objects.'\n      );\n      // The following loop is **the** hot loop, so we keep it as light as\n      // possible.\n      for (var i = 0, l = this.tracked.length; i < l; ++i) {\n        if (this.tracked[i] === a) break;\n      }\n      // We only update the number of comparisons after the loop, exploiting the\n      // fact that `i` is still in scope and contains the approximate number of\n      // comparisons made. `this.lookups` can exceed `comparisonLimit` as a\n      // result, but this is unproblematic.\n      this.lookups += i;\n      // Return the index where `a` was found, plus one so it doesn't look\n      // falsy.\n      return i < l ? i + 1 : false;\n    },\n    // The `match` method takes one argument more than the corresponding method","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/jashkenas/underscore/blob/e70d5bd070f1d883b40e786a955a61e4f4b3c2c6/modules/isEqual.js#L68-L104","documentation":"_.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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Break cycles before comparing: strip or replace circular references (e.g. with a cycle-removal helper) and then call _.isEqual.","Compare a shallow or bounded-depth projection instead: pick specific keys or cap the nesting level you care about.","Use a diff/deep-equal library that supports cyclic data natively (e.g. one tracking visited nodes with WeakMap/WeakSet) for graph-shaped data.","Compare canonical serializations (JSON.stringify with a cycle-safe replacer) when semantic identity is what matters.","If the size is the problem, chunk the comparison: compare top-level keys/sections with separate _.isEqual calls."],"exampleFix":"// before\n_.isEqual(circularA, circularB);\n// RangeError: Comparison limit exceeded...\n\n// after\nconst strip = (v, seen = new WeakSet()) => {\n  if (v && typeof v === 'object') {\n    if (seen.has(v)) return '[circular]';\n    seen.add(v);\n    return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, strip(x, seen)]));\n  }\n  return v;\n};\n_.isEqual(strip(circularA), strip(circularB));","handlingStrategy":"try-catch","validationCode":"function detectCycle(v, seen = new WeakSet()) {\n  if (v && typeof v === 'object') {\n    if (seen.has(v)) return true;\n    seen.add(v);\n    return Object.values(v).some((x) => detectCycle(x, seen));\n  }\n  return false;\n}\n// if (detectCycle(a) || detectCycle(b)) use a cycle-safe comparison instead","typeGuard":"const isCycleSafe = (v, seen = new WeakSet()) => {\n  if (v === null || typeof v !== 'object') return true;\n  if (seen.has(v)) return false;\n  seen.add(v);\n  return Object.values(v).every((x) => isCycleSafe(x, seen));\n};","tryCatchPattern":"try {\n  return _.isEqual(a, b);\n} catch (e) {\n  if (e instanceof RangeError && /Comparison limit exceeded/.test(e.message)) {\n    return deepEqualCycleSafe(a, b); // fallback comparator using WeakSet of visited nodes\n  }\n  throw e;\n}","preventionTips":["Keep object graphs acyclic (no parent back-references) or strip cycles before deep comparisons.","Avoid calling _.isEqual on very deep structures; compare projections or selected keys instead.","Use a cycle-aware deep-equal implementation for entity/ORM graphs.","For large configs, diff section-by-section with separate isEqual calls to stay under the limit.","Wrap isEqual calls over untrusted/dynamic data in try/catch so a RangeError degrades gracefully."],"tags":["rangeerror","deep-equality","circular-reference","performance","recursion-limit"],"backgroundTag":"circular-reference-detected","analyzedSha":"e70d5bd070f1d883b40e786a955a61e4f4b3c2c6","analyzedAt":"2026-08-29T10:08:07.718Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}