88

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?

Mike Samuel
  • 118,113
  • 30
  • 216
  • 245
Mads Mobæk
  • 34,762
  • 20
  • 71
  • 78

16 Answers16

66

Pulled from http://blog.vjeux.com/2011/javascript/cyclic-object-detection.html. One line added to detect where the cycle is. Paste this into the Chrome dev tools:

function isCyclic (obj) {
  var seenObjects = [];

  function detect (obj) {
    if (obj && typeof obj === 'object') {
      if (seenObjects.indexOf(obj) !== -1) {
        return true;
      }
      seenObjects.push(obj);
      for (var key in obj) {
        if (obj.hasOwnProperty(key) && detect(obj[key])) {
          console.log(obj, 'cycle at ' + key);
          return true;
        }
      }
    }
    return false;
  }

  return detect(obj);
}

Here's the test:

> a = {}
> b = {}
> a.b = b; b.a = a;
> isCyclic(a)
  Object {a: Object}
   "cycle at a"
  Object {b: Object}
   "cycle at b"
  true
JellicleCat
  • 28,480
  • 24
  • 109
  • 162
Trey Mack
  • 4,215
  • 2
  • 25
  • 31
  • Don't forget to add the root JSON object itself to `seenObjects`. It should be `var seenObjects = [obj];`. – Aadit M Shah Feb 19 '13 at 16:37
  • @AaditMShah, since `detect` is called on the input object, that would cause it to immediately exit claiming there is a cycle directly from the input object to itself. – Mike Samuel Feb 19 '13 at 16:44
  • @MikeSamuel - Ah. Alright. I didn't see that it was being added inside the `detect` function. – Aadit M Shah Feb 19 '13 at 16:48
  • 4
    This false positives on null and isn't recursive – Andy Ray Jun 24 '14 at 05:27
  • 37
    Just because a reference is used more than once doesn't necessarily mean that it's cyclic. `var x = {}; JSON.stringify([x,x])` is fine... while `var x = {}; x.x = x; JSON.stringify(x);` is not. – canon May 14 '15 at 13:58
  • Did not know you could stick in a function ontop like that and have it return a value. Very sneaky tmack, thanks for this!! – NiCk Newman Jun 04 '15 at 04:35
  • You can store a console.logged variable in dev tools by right clicking it. – Brampage Mar 30 '17 at 20:21
  • 1
    For some reason when I use this solution, it gives me an error: `obj.hasOwnProperty is not a function`. This is running inside a script loaded by the browser. – Ayush Goel Mar 21 '18 at 18:11
  • This wrong! Those who don't read the comment will naive copy paste this – TSR Sep 09 '20 at 11:17
  • 3
    this is not a cyclic `z = {}; x = {x1: z, x2: z};` but your code `isCyclic(z)` returns `true` – TSR Sep 09 '20 at 11:18
  • Or even better, just use the set because set has indexes – TSR Sep 09 '20 at 11:37
52

@tmack's answer is definitely what I was looking for when I found this question!

Unfortunately it returns many false positives - it returns true if an object is replicated in the JSON, which isn't the same as circularity. Circularity means that an object is its own child, e.g.

obj.key1.key2.[...].keyX === obj

I modified the original answer, and this is working for me:

function isCyclic(obj) {
  var keys = [];
  var stack = [];
  var stackSet = new Set();
  var detected = false;

  function detect(obj, key) {
    if (obj && typeof obj != 'object') { return; }

    if (stackSet.has(obj)) { // it's cyclic! Print the object and its locations.
      var oldindex = stack.indexOf(obj);
      var l1 = keys.join('.') + '.' + key;
      var l2 = keys.slice(0, oldindex + 1).join('.');
      console.log('CIRCULAR: ' + l1 + ' = ' + l2 + ' = ' + obj);
      console.log(obj);
      detected = true;
      return;
    }

    keys.push(key);
    stack.push(obj);
    stackSet.add(obj);
    for (var k in obj) { //dive on the object's children
      if (Object.prototype.hasOwnProperty.call(obj, k)) { detect(obj[k], k); }
    }

    keys.pop();
    stack.pop();
    stackSet.delete(obj);
    return;
  }

  detect(obj, 'obj');
  return detected;
}

Here are a few very simple tests:

var root = {}
var leaf = {'isleaf':true};
var cycle2 = {l:leaf};
var cycle1 = {c2: cycle2, l:leaf};
cycle2.c1 = cycle1
root.leaf = leaf

isCyclic(cycle1); // returns true, logs "CIRCULAR: obj.c2.c1 = obj"
isCyclic(cycle2); // returns true, logs "CIRCULAR: obj.c1.c2 = obj"
isCyclic(leaf); // returns false
isCyclic(root); // returns false
Aaron V
  • 6,596
  • 5
  • 28
  • 31
  • 2
    Adding memoization to make this O(N) is left as an exercise for the reader :) – Aaron V Jan 20 '16 at 19:46
  • I added a TypeScript version of this answer as a new answer. See below. – mvermand Sep 28 '17 at 05:04
  • 4
    `if (typeof obj != 'object') { return; }` should be `if (obj && typeof obj != 'object') { return; }` because `typeof null == "object"`. – jcubic Sep 29 '17 at 07:55
  • @AaronV could you give a hint where you would add memoization? I'm not seeing where it could come in handy. Thanks. – Willwsharp Jun 13 '18 at 15:23
  • 1
    @Willwsharp - it's been a while, but I believe the idea was that it's definitely possible to encounter the same object more than once, even in a non-circular structure. So, in the worst case, the code I wrote here would process the same objects over and over again, making this needlessly O(N^2). You could use memoization to store each object's children and prevent reprocessing. – Aaron V Jun 13 '18 at 19:12
  • 1
    @AaronV oh awesome! I worked a little more on it and I came to the same conclusion so glad to hear I was on the right path; thanks! – Willwsharp Jun 14 '18 at 13:38
  • thank you for the great example, but how to be in the case when you don't know where the object with cyclic structure is it? because in my case i can not catch it already a few months, my app has a lot of third party libraries, and many of them use JSON.stringify. what could you recommend for it? – jocoders Feb 03 '20 at 09:03
  • instead of using both a stack (array) and a set, why not use an array only ? – TSR Sep 09 '20 at 11:33
  • @TSR - because checking if an element exists is O(1) in a set but O(N) in an array – Aaron V Sep 09 '20 at 19:20
  • Okay good point @AaronV but why not just use a set only then ? Then to get the `oldindex` just convert the set into an array using `Array.from(set)` because set are ordered by order of insertion, – TSR Sep 09 '20 at 21:41
  • somehow this code went into an endless loop for me – Marquez Dec 10 '21 at 18:30
22

Here is MDN's approach to detecting and fixing circular references when using JSON.stringify() on circular objects: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value :

In a circular structure like the following

var circularReference = {otherData: 123};
circularReference.myself = circularReference;

JSON.stringify() will fail:

JSON.stringify(circularReference);
// TypeError: cyclic object value

To serialize circular references you can use a library that supports them (e.g. cycle.js) or implement a solution by yourself, which will require finding and replacing (or removing) the cyclic references by serializable values.

The snippet below illustrates how to find and filter (thus causing data loss) a cyclic reference by using the replacer parameter of JSON.stringify():

const getCircularReplacer = () => {
      const seen = new WeakSet();
      return (key, value) => {
        if (typeof value === "object" && value !== null) {
          if (seen.has(value)) {
            return;
          }
          seen.add(value);
        }
        return value;
      };
    };

JSON.stringify(circularReference, getCircularReplacer());
// {"otherData":123}
Mattb150
  • 229
  • 2
  • 3
  • 2
    This is the best answer. Everyone wants to write their own homegrown code. On the other hand, MDN is authoritative. If this code is too specific (it's meant to go in JSON.stringify), then check out the code they reference, cycle.js, which is written by Someone Who Knows What They Are Doing. – Nate Apr 26 '21 at 19:31
  • 2
    Beautiful. F**kin glue this answer to the front page. – mad.meesh Apr 30 '22 at 19:14
  • This has the same issue as trey mack's answer. Duplicated objects in the object are not inherently circular. An array with 2 copies of the same object would fail this check, while not being at all circular – Adam Yost Jun 07 '23 at 00:24
9

CircularReferenceDetector

Here is my CircularReferenceDetector class which outputs all the property stack information where the circularly referenced value is actually located at and also shows where the culprit references are.

This is especially useful for huge structures where it is not obvious by the key which value is the source of the harm.

It outputs the circularly referenced value stringified but all references to itself replaced by "[Circular object --- fix me]".

Usage:
CircularReferenceDetector.detectCircularReferences(value);

Note: Remove the Logger.* statements if you do not want to use any logging or do not have a logger available.

Technical Explanation:
The recursive function goes through all properties of the object and tests if JSON.stringify succeeds on them or not. If it does not succeed (circular reference), then it tests if it succeeds by replacing value itself with some constant string. This would mean that if it succeeds using this replacer, this value is the being circularly referenced value. If it is not, it recursively goes through all properties of that object.

Meanwhile it also tracks the property stack to give you information where the culprit value is located at.

Typescript

import {Logger} from "../Logger";

export class CircularReferenceDetector {

    static detectCircularReferences(toBeStringifiedValue: any, serializationKeyStack: string[] = []) {
        Object.keys(toBeStringifiedValue).forEach(key => {
            var value = toBeStringifiedValue[key];

            var serializationKeyStackWithNewKey = serializationKeyStack.slice();
            serializationKeyStackWithNewKey.push(key);
            try {
                JSON.stringify(value);
                Logger.debug(`path "${Util.joinStrings(serializationKeyStack)}" is ok`);
            } catch (error) {
                Logger.debug(`path "${Util.joinStrings(serializationKeyStack)}" JSON.stringify results in error: ${error}`);

                var isCircularValue:boolean;
                var circularExcludingStringifyResult:string = "";
                try {
                    circularExcludingStringifyResult = JSON.stringify(value, CircularReferenceDetector.replaceRootStringifyReplacer(value), 2);
                    isCircularValue = true;
                } catch (error) {
                    Logger.debug(`path "${Util.joinStrings(serializationKeyStack)}" is not the circular source`);
                    CircularReferenceDetector.detectCircularReferences(value, serializationKeyStackWithNewKey);
                    isCircularValue = false;
                }
                if (isCircularValue) {
                    throw new Error(`Circular reference detected:\nCircularly referenced value is value under path "${Util.joinStrings(serializationKeyStackWithNewKey)}" of the given root object\n`+
                        `Calling stringify on this value but replacing itself with [Circular object --- fix me] ( <-- search for this string) results in:\n${circularExcludingStringifyResult}\n`);
                }
            }
        });
    }

    private static replaceRootStringifyReplacer(toBeStringifiedValue: any): any {
        var serializedObjectCounter = 0;

        return function (key: any, value: any) {
            if (serializedObjectCounter !== 0 && typeof(toBeStringifiedValue) === 'object' && toBeStringifiedValue === value) {
                Logger.error(`object serialization with key ${key} has circular reference to being stringified object`);
                return '[Circular object --- fix me]';
            }

            serializedObjectCounter++;

            return value;
        }
    }
}

export class Util {

    static joinStrings(arr: string[], separator: string = ":") {
        if (arr.length === 0) return "";
        return arr.reduce((v1, v2) => `${v1}${separator}${v2}`);
    }

}

Compiled JavaScript from TypeScript

"use strict";
const Logger_1 = require("../Logger");
class CircularReferenceDetector {
    static detectCircularReferences(toBeStringifiedValue, serializationKeyStack = []) {
        Object.keys(toBeStringifiedValue).forEach(key => {
            var value = toBeStringifiedValue[key];
            var serializationKeyStackWithNewKey = serializationKeyStack.slice();
            serializationKeyStackWithNewKey.push(key);
            try {
                JSON.stringify(value);
                Logger_1.Logger.debug(`path "${Util.joinStrings(serializationKeyStack)}" is ok`);
            }
            catch (error) {
                Logger_1.Logger.debug(`path "${Util.joinStrings(serializationKeyStack)}" JSON.stringify results in error: ${error}`);
                var isCircularValue;
                var circularExcludingStringifyResult = "";
                try {
                    circularExcludingStringifyResult = JSON.stringify(value, CircularReferenceDetector.replaceRootStringifyReplacer(value), 2);
                    isCircularValue = true;
                }
                catch (error) {
                    Logger_1.Logger.debug(`path "${Util.joinStrings(serializationKeyStack)}" is not the circular source`);
                    CircularReferenceDetector.detectCircularReferences(value, serializationKeyStackWithNewKey);
                    isCircularValue = false;
                }
                if (isCircularValue) {
                    throw new Error(`Circular reference detected:\nCircularly referenced value is value under path "${Util.joinStrings(serializationKeyStackWithNewKey)}" of the given root object\n` +
                        `Calling stringify on this value but replacing itself with [Circular object --- fix me] ( <-- search for this string) results in:\n${circularExcludingStringifyResult}\n`);
                }
            }
        });
    }
    static replaceRootStringifyReplacer(toBeStringifiedValue) {
        var serializedObjectCounter = 0;
        return function (key, value) {
            if (serializedObjectCounter !== 0 && typeof (toBeStringifiedValue) === 'object' && toBeStringifiedValue === value) {
                Logger_1.Logger.error(`object serialization with key ${key} has circular reference to being stringified object`);
                return '[Circular object --- fix me]';
            }
            serializedObjectCounter++;
            return value;
        };
    }
}
exports.CircularReferenceDetector = CircularReferenceDetector;
class Util {
    static joinStrings(arr, separator = ":") {
        if (arr.length === 0)
            return "";
        return arr.reduce((v1, v2) => `${v1}${separator}${v2}`);
    }
}
exports.Util = Util;
Ulysse BN
  • 10,116
  • 7
  • 54
  • 82
Thomas
  • 650
  • 9
  • 9
  • 2
    Should publish this to npm – Jack Murphy Apr 18 '17 at 14:23
  • This causes and infinite loop, logging thousands of entries to the console, one of the other answers also caused and infinite loop. However, the original implementation of isCyclic in another of the answers did not cause an infinite loop. – thephpdev Nov 30 '18 at 10:57
  • thank you for the great example, but how to be in the case when you don't know where the object with cyclic structure is it? because in my case i can not catch it already a few months, my app has a lot of third party libraries, and many of them use JSON.stringify. what could you recommend for it? – jocoders Feb 03 '20 at 09:02
  • How to use this ? can u bring some an example? – Dimas Lanjaka Jun 12 '23 at 15:00
7

This is a fix for both @Trey Mack and @Freddie Nfbnm answers on the typeof obj != 'object' condition. Instead it should test if the obj value is not instance of object, so that it can also work when checking values with object familiarity (for example, functions and symbols (symbols aren't instance of object, but still addressed, btw.)).

I'm posting this as an answer since I can't comment in this StackExchange account yet.

PS.: feel free to request me to delete this answer.

function isCyclic(obj) {
  var keys = [];
  var stack = [];
  var stackSet = new Set();
  var detected = false;

  function detect(obj, key) {
    if (!(obj instanceof Object)) { return; } // Now works with other
                                              // kinds of object.

    if (stackSet.has(obj)) { // it's cyclic! Print the object and its locations.
      var oldindex = stack.indexOf(obj);
      var l1 = keys.join('.') + '.' + key;
      var l2 = keys.slice(0, oldindex + 1).join('.');
      console.log('CIRCULAR: ' + l1 + ' = ' + l2 + ' = ' + obj);
      console.log(obj);
      detected = true;
      return;
    }

    keys.push(key);
    stack.push(obj);
    stackSet.add(obj);
    for (var k in obj) { //dive on the object's children
      if (obj.hasOwnProperty(k)) { detect(obj[k], k); }
    }

    keys.pop();
    stack.pop();
    stackSet.delete(obj);
    return;
  }

  detect(obj, 'obj');
  return detected;
}
7

You can also use JSON.stringify with try/catch

function hasCircularDependency(obj)
{
    try
    {
        JSON.stringify(obj);
    }
    catch(e)
    {
        return e.includes("Converting circular structure to JSON"); 
    }
    return false;
}

Demo

function hasCircularDependency(obj) {
  try {
    JSON.stringify(obj);
  } catch (e) {
    return String(e).includes("Converting circular structure to JSON");
  }
  return false;
}

var a = {b:{c:{d:""}}};
console.log(hasCircularDependency(a));
a.b.c.d = a;
console.log(hasCircularDependency(a));
gurvinder372
  • 66,980
  • 10
  • 72
  • 94
  • 7
    Are you answering the question with the question itself? – hudidit Dec 10 '19 at 11:40
  • 1
    OP already knows that it's a circular reference causing an error, and he said in the question ``I want to find the **cause** of this circular reference`` (emphasis mine). So you answer does not answer OP's question at all. – Mike Scotty Jul 07 '20 at 13:30
  • 1
    @hudidit I want to flag your comment as funny. Also, for further but similarly not important information, I am not able to flag this my own comment as anything. – stackuser83 Mar 31 '21 at 17:40
7

Here is a Node ES6 version mixed from the answers from @Aaron V and @user4976005, it fixes the problem with the call to hasOwnProperty:

const isCyclic = (obj => {
  const keys = []
  const stack = []
  const stackSet = new Set()
  let detected = false

  const detect = ((object, key) => {
    if (!(object instanceof Object))
      return

    if (stackSet.has(object)) { // it's cyclic! Print the object and its locations.
      const oldindex = stack.indexOf(object)
      const l1 = `${keys.join('.')}.${key}`
      const l2 = keys.slice(0, oldindex + 1).join('.')
      console.log(`CIRCULAR: ${l1} = ${l2} = ${object}`)
      console.log(object)
      detected = true
      return
    }

    keys.push(key)
    stack.push(object)
    stackSet.add(object)
    Object.keys(object).forEach(k => { // dive on the object's children
      if (k && Object.prototype.hasOwnProperty.call(object, k))
        detect(object[k], k)
    })

    keys.pop()
    stack.pop()
    stackSet.delete(object)
  })

  detect(obj, 'obj')
  return detected
})
dkurzaj
  • 346
  • 4
  • 13
6

There's a lot of answers here, but I thought I'd add my solution to the mix. It's similar to @Trey Mack's answer, but that solution takes O(n^2). This version uses WeakMap instead of an array, improving the time to O(n).

function isCyclic(object) {
   const seenObjects = new WeakMap(); // use to keep track of which objects have been seen.

   function detectCycle(obj) {
      // If 'obj' is an actual object (i.e., has the form of '{}'), check
      // if it's been seen already.
      if (Object.prototype.toString.call(obj) == '[object Object]') {

         if (seenObjects.has(obj)) {
            return true;
         }

         // If 'obj' hasn't been seen, add it to 'seenObjects'.
         // Since 'obj' is used as a key, the value of 'seenObjects[obj]'
         // is irrelevent and can be set as literally anything you want. I 
         // just went with 'undefined'.
         seenObjects.set(obj, undefined);

         // Recurse through the object, looking for more circular references.
         for (var key in obj) {
            if (detectCycle(obj[key])) {
               return true;
            }
         }

      // If 'obj' is an array, check if any of it's elements are
      // an object that has been seen already.
      } else if (Array.isArray(obj)) {
         for (var i in obj) {
            if (detectCycle(obj[i])) {
               return true;
            }
         }
      }

      return false;
   }

   return detectCycle(object);
}

And this is what it looks like in action.

> var foo = {grault: {}};
> detectCycle(foo);
false
> foo.grault = foo;
> detectCycle(foo);
true
> var bar = {};
> detectCycle(bar);
false
> bar.plugh = [];
> bar.plugh.push(bar);
> detectCycle(bar);
true
darksinge
  • 1,828
  • 1
  • 21
  • 32
2

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;
  }
 }
SzybkiSasza
  • 1,591
  • 12
  • 27
  • 1
    I Was working on a generic method to deep clone objects, you helped a lot, thak you so much – Mateus Martins Feb 10 '21 at 13:23
  • Nice answer, I had hopes, but got: TypeError: Cannot add property Symbol(VISITED_MARK), object is not extensible . Explanation here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cant_define_property_object_not_extensible – Marquez Dec 10 '21 at 18:47
0

I just made this. It may be dirty, but works anyway... :P

function dump(orig){
  var inspectedObjects = [];
  console.log('== DUMP ==');
  (function _dump(o,t){
    console.log(t+' Type '+(typeof o));
    for(var i in o){
      if(o[i] === orig){
        console.log(t+' '+i+': [recursive]'); 
        continue;
      }
      var ind = 1+inspectedObjects.indexOf(o[i]);
      if(ind>0) console.log(t+' '+i+':  [already inspected ('+ind+')]');
      else{
        console.log(t+' '+i+': ('+inspectedObjects.push(o[i])+')');
        _dump(o[i],t+'>>');
      }
    }
  }(orig,'>'));
}

Then

var a = [1,2,3], b = [a,4,5,6], c = {'x':a,'y':b};
a.push(c); dump(c);

Says

== DUMP ==
> Type object
> x: (1)
>>> Type object
>>> 0: (2)
>>>>> Type number
>>> 1: (3)
>>>>> Type number
>>> 2: (4)
>>>>> Type number
>>> 3: [recursive]
> y: (5)
>>> Type object
>>> 0:  [already inspected (1)]
>>> 1: (6)
>>>>> Type number
>>> 2: (7)
>>>>> Type number
>>> 3: (8)
>>>>> Type number

This tells that c.x[3] is equal to c, and c.x = c.y[0].

Or, a little edit to this function can tell you what you need...

function findRecursive(orig){
  var inspectedObjects = [];
  (function _find(o,s){
    for(var i in o){
      if(o[i] === orig){
        console.log('Found: obj.'+s.join('.')+'.'+i); 
        return;
      }
      if(inspectedObjects.indexOf(o[i])>=0) continue;
      else{
        inspectedObjects.push(o[i]);
        s.push(i); _find(o[i],s); s.pop(i);
      }
    }
  }(orig,[]));
}
JiminP
  • 2,136
  • 19
  • 26
  • The `inspectedObjects` array should contain the original JSON object itself (i.e. it should be `var inspectedObjects = [orig];`). – Aadit M Shah Feb 19 '13 at 16:39
  • I intentionally separated checking about the original object (by `if(o[i] === orig)`) and other objects (by `inspectedObjects.indexOf(o[i])`). – JiminP Feb 19 '13 at 17:02
  • 2
    Just because a reference is used more than once doesn't necessarily mean that it's cyclic. `var x = {}; JSON.stringify([x,x])` is fine... while `var x = {}; x.x = x; JSON.stringify(x);` is not. – canon May 14 '15 at 13:57
0

Here is @Thomas's answer adapted for node:

const {logger} = require("../logger")
// Or: const logger = {debug: (...args) => console.log.call(console.log, args) }

const joinStrings = (arr, separator) => {
  if (arr.length === 0) return "";
  return arr.reduce((v1, v2) => `${v1}${separator}${v2}`);
}

exports.CircularReferenceDetector = class CircularReferenceDetector {

  detectCircularReferences(toBeStringifiedValue, serializationKeyStack = []) {
    Object.keys(toBeStringifiedValue).forEach(key => {
      let value = toBeStringifiedValue[key];

      let serializationKeyStackWithNewKey = serializationKeyStack.slice();
      serializationKeyStackWithNewKey.push(key);
      try {
        JSON.stringify(value);
        logger.debug(`path "${joinStrings(serializationKeyStack)}" is ok`);
      } catch (error) {
        logger.debug(`path "${joinStrings(serializationKeyStack)}" JSON.stringify results in error: ${error}`);

        let isCircularValue;
        let circularExcludingStringifyResult = "";
        try {
          circularExcludingStringifyResult = JSON.stringify(value, this.replaceRootStringifyReplacer(value), 2);
          isCircularValue = true;
        } catch (error) {
          logger.debug(`path "${joinStrings(serializationKeyStack)}" is not the circular source`);
          this.detectCircularReferences(value, serializationKeyStackWithNewKey);
          isCircularValue = false;
        }
        if (isCircularValue) {
          throw new Error(`Circular reference detected:\nCircularly referenced value is value under path "${joinStrings(serializationKeyStackWithNewKey)}" of the given root object\n`+
              `Calling stringify on this value but replacing itself with [Circular object --- fix me] ( <-- search for this string) results in:\n${circularExcludingStringifyResult}\n`);
        }
      }
    });
  }

  replaceRootStringifyReplacer(toBeStringifiedValue) {
    let serializedObjectCounter = 0;

    return function (key, value) {
      if (serializedObjectCounter !== 0 && typeof(toBeStringifiedValue) === 'object' && toBeStringifiedValue === value) {
        logger.error(`object serialization with key ${key} has circular reference to being stringified object`);
        return '[Circular object --- fix me]';
      }

      serializedObjectCounter++;

      return value;
    }
  }
}
Carl G
  • 17,394
  • 14
  • 91
  • 115
0

I converted the answer of Freddie Nfbnm to TypeScript:

export class JsonUtil {

    static isCyclic(json) {
        const keys = [];
        const stack = [];
        const stackSet = new Set();
        let detected = false;

        function detect(obj, key) {
            if (typeof obj !== 'object') {
                return;
            }

            if (stackSet.has(obj)) { // it's cyclic! Print the object and its locations.
                const oldIndex = stack.indexOf(obj);
                const l1 = keys.join('.') + '.' + key;
                const l2 = keys.slice(0, oldIndex + 1).join('.');
                console.log('CIRCULAR: ' + l1 + ' = ' + l2 + ' = ' + obj);
                console.log(obj);
                detected = true;
                return;
            }

            keys.push(key);
            stack.push(obj);
            stackSet.add(obj);
            for (const k in obj) { // dive on the object's children
                if (obj.hasOwnProperty(k)) {
                    detect(obj[k], k);
                }
            }

            keys.pop();
            stack.pop();
            stackSet.delete(obj);
            return;
        }

        detect(json, 'obj');
        return detected;
    }

}
mvermand
  • 5,829
  • 7
  • 48
  • 74
0

Just to throw my version into the mix... below is a remix of @dkurzaj 's code (which is itself a remix of @Aaron V 's, @user4976005 's, @Trey Mack 's and finally @Freddie Nfbnm 's [removed?] code) plus @darksinge 's WeakMap idea. So... this thread's Megamix, I guess :)

In my version, a report (rather than console.log'ed entries) is optionally returned as an array of objects. If a report is not required, testing stops on the first sighting of a circular reference (a'la @darksinge 's code).

Further, hasOwnProperty has been removed as Object.keys returns only hasOwnProperty properties (see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys ).

function isCyclic(x, bReturnReport) {
    var a_sKeys = [],
        a_oStack = [],
        wm_oSeenObjects = new WeakMap(), //# see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
        oReturnVal = {
            found: false,
            report: []
        }
    ;

    //# Setup the recursive logic to locate any circular references while kicking off the initial call
    (function doIsCyclic(oTarget, sKey) {
        var a_sTargetKeys, sCurrentKey, i;

        //# If we've seen this oTarget before, flip our .found to true
        if (wm_oSeenObjects.has(oTarget)) {
            oReturnVal.found = true;

            //# If we are to bReturnReport, add the entries into our .report
            if (bReturnReport) {
                oReturnVal.report.push({
                    instance: oTarget,
                    source: a_sKeys.slice(0, a_oStack.indexOf(oTarget) + 1).join('.'),
                    duplicate: a_sKeys.join('.') + "." + sKey
                });
            }
        }
        //# Else if oTarget is an instanceof Object, determine the a_sTargetKeys and .set our oTarget into the wm_oSeenObjects
        else if (oTarget instanceof Object) {
            a_sTargetKeys = Object.keys(oTarget);
            wm_oSeenObjects.set(oTarget /*, undefined*/);

            //# If we are to bReturnReport, .push the  current level's/call's items onto our stacks
            if (bReturnReport) {
                if (sKey) { a_sKeys.push(sKey) };
                a_oStack.push(oTarget);
            }

            //# Traverse the a_sTargetKeys, pulling each into sCurrentKey as we go
            //#     NOTE: If you want all properties, even non-enumerables, see Object.getOwnPropertyNames() so there is no need to call .hasOwnProperty (per: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys)
            for (i = 0; i < a_sTargetKeys.length; i++) {
                sCurrentKey = a_sTargetKeys[i];

                //# If we've already .found a circular reference and we're not bReturnReport, fall from the loop
                if (oReturnVal.found && !bReturnReport) {
                    break;
                }
                //# Else if the sCurrentKey is an instanceof Object, recurse to test
                else if (oTarget[sCurrentKey] instanceof Object) {
                    doIsCyclic(oTarget[sCurrentKey], sCurrentKey);
                }
            }

            //# .delete our oTarget into the wm_oSeenObjects
            wm_oSeenObjects.delete(oTarget);

            //# If we are to bReturnReport, .pop the current level's/call's items off our stacks
            if (bReturnReport) {
                if (sKey) { a_sKeys.pop() };
                a_oStack.pop();
            }
        }
    }(x, '')); //# doIsCyclic

    return (bReturnReport ? oReturnVal.report : oReturnVal.found);
}
Campbeln
  • 2,880
  • 3
  • 33
  • 33
0

Most of the other answers only show how to detect that an object-tree has a circular-reference -- they don't tell you how to fix those circular references (ie. replacing the circular-reference values with, eg. undefined).

The below is the function I use to replace all circular-references with undefined:

export const specialTypeHandlers_default = [
    // Set and Map are included by default, since JSON.stringify tries (and fails) to serialize them by default
    {type: Set, keys: a=>a.keys(), get: (a, key)=>key, delete: (a, key)=>a.delete(key)},
    {type: Map, keys: a=>a.keys(), get: (a, key)=>a.get(key), delete: (a, key)=>a.set(key, undefined)},
];
export function RemoveCircularLinks(node, specialTypeHandlers = specialTypeHandlers_default, nodeStack_set = new Set()) {
    nodeStack_set.add(node);

    const specialHandler = specialTypeHandlers.find(a=>node instanceof a.type);
    for (const key of specialHandler ? specialHandler.keys(node) : Object.keys(node)) {
        const value = specialHandler ? specialHandler.get(node, key) : node[key];
        // if the value is already part of visited-stack, delete the value (and don't tunnel into it)
        if (nodeStack_set.has(value)) {
            if (specialHandler) specialHandler.delete(node, key);
            else node[key] = undefined;
        }
        // else, tunnel into it, looking for circular-links at deeper levels
        else if (typeof value == "object" && value != null) {
            RemoveCircularLinks(value, specialTypeHandlers, nodeStack_set);
        }
    }

    nodeStack_set.delete(node);
}

For use with JSON.stringify specifically, simply call the function above prior to the stringification (note that it does mutate the passed-in object):

const objTree = {normalProp: true};
objTree.selfReference = objTree;
RemoveCircularLinks(objTree); // without this line, the JSON.stringify call errors
console.log(JSON.stringify(objTree));
Venryx
  • 15,624
  • 10
  • 70
  • 96
-1

Try using console.log() on the chrome/firefox browser to identify where the issue encountered.

On Firefox using Firebug plugin, you can debug your javascript line by line.

Update:

Refer below example of circular reference issue and which has been handled:-

// JSON.stringify, avoid TypeError: Converting circular structure to JSON
// Demo: Circular reference
var o = {};
o.o = o;

var cache = [];
JSON.stringify(o, function(key, value) {
    if (typeof value === 'object' && value !== null) {
        if (cache.indexOf(value) !== -1) {
            // Circular reference found, discard key
            alert("Circular reference found, discard key");
            return;
        }
        alert("value = '" + value + "'");
        // Store value in our collection
        cache.push(value);
    }
    return value;
});
cache = null; // Enable garbage collection

var a = {b:1};
var o = {};
o.one = a;
o.two = a;
// one and two point to the same object, but two is discarded:
JSON.stringify(o);

var obj = {
  a: "foo",
  b: obj
};

var replacement = {"b":undefined};

alert("Result : " + JSON.stringify(obj,replacement));

Refer example LIVE DEMO

Siva Charan
  • 17,940
  • 9
  • 60
  • 95
  • 5
    You mean *manually* searching for the reference? That's more the last resort than a good solution. – Šime Vidas Feb 19 '13 at 16:12
  • 3
    *"you can debug your javascript line by line"* - The bulit-in `JSON.stringify()` is not implemented in JavaScript, but native code. You can't debug it with Firebug. – Šime Vidas Feb 19 '13 at 16:15
  • 1
    There's lots of libraries that provide a stringify() function in javascript for older browsers. Maybe you could use one of those? That would presumably break at the exact point it's parsing the problem reference. – Ask About Monica Feb 19 '13 at 16:26
  • 4
    Just because a reference is used more than once doesn't necessarily mean that it's cyclic. `var x = {}; JSON.stringify([x,x])` is fine... while `var x = {}; x.x = x; JSON.stringify(x);` is not. – canon May 14 '15 at 13:52
-1

if you just need to see the content of that circular object, just use console.table(circularObj)

  • This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/late-answers/30845243) – Luca Kiebel Jan 20 '22 at 21:40