Detecting and fixing circular references in JavaScript

Given I have a circular reference in a large JavaScript object

And I try JSON.stringify(problematicObject)

And the browser throws

"TypeError: Converting circular structure to JSON"

(which is expected)

Then I want to find the cause of this circular reference, preferably using Chrome developer tools? Is this possible? How do you find and fix circular references in a large object?

Answers:

Answer

You can also use Symbols - thanks to that approach you won't have to mutate properties of the original object, apart from adding symbol for marking visited node.

It's cleaner and should be faster than gathering node properties and comparing with the object. It also has optional depth limitation if you don't want to serialize big nested values:

// Symbol used to mark already visited nodes - helps with circular dependencies
const visitedMark = Symbol('VISITED_MARK');

const MAX_CLEANUP_DEPTH = 10;

function removeCirculars(obj, depth = 0) {
  if (!obj) {
    return obj;
  }

  // Skip condition - either object is falsy, was visited or we go too deep
  const shouldSkip = !obj || obj[visitedMark] || depth > MAX_CLEANUP_DEPTH;

  // Copy object (we copy properties from it and mark visited nodes)
  const originalObj = obj;
  let result = {};

  Object.keys(originalObj).forEach((entry) => {
    const val = originalObj[entry];

    if (!shouldSkip) {
      if (typeof val === 'object') { // Value is an object - run object sanitizer
        originalObj[visitedMark] = true; // Mark current node as "seen" - will stop from going deeper into circulars
        const nextDepth = depth + 1;
        result[entry] = removeCirculars(val, nextDepth);
      } else {
        result[entry] = val;
      }
    } else {
      result = 'CIRCULAR';
    }
  });

  return result;
}

This will result in an object that has all the circular dependencies stripped and also does not go deeper than given MAX_CLEANUP_DEPTH.

Using symbols is safe as long as you don't do any meta-programming stuff on the object - they are transparent and they are not enumerable, hence - they will not show in any standard operations on the object.

Also, returning a new, cleaned up object has an advantage of not mutating the original one if you need to perform any additional operations on it.

If you don't want CIRCULAR marking, you can just modify the code a bit, hence skipping object before actually performing operations on it (inside the loop):

 originalObj[visitedMark] = true; // Mark current node as "seen" - will stop from going deeper into circulars
 const val = originalObj[entry];

 // Skip condition - either object is falsy, was visited or we go too deep
 const shouldSkip = val[visitedMark] || depth > MAX_SANITIZATION_DEPTH;

 if (!shouldSkip) {
   if (typeof val === 'object') { // Value is an object - run object sanitizer
    const nextDepth = depth + 1;
    result[entry] = removeCirculars(val, nextDepth);
  } else {
    result[entry] = val;
  }
 }

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.