TypeError: Converting circular structure to JSON

自分自身を辿れるオブジェクトを JSON.stringify() に渡すと出ます。親子の相互参照、 DOM 要素、通信のオブジェクト。どこで輪になっているかの見つけ方と、3つの直し方を載せています。

実行して検証済み処理系Node 22.22.3対応ES5難易度●●○○○目安8更新

自分自身に辿り着けるオブジェクトを文字列にしようとしたときに出ます。

javascript
const node = { name: 'a' };

node.self = node;

JSON.stringify(node);
TypeError: Converting circular structure to JSON --> starting at object with constructor 'Object' --- property 'self' closes the circle
javascript
const parent = { name: '親' };
const child = { name: '子', parent };

parent.child = child;

JSON.stringify(parent);
TypeError: Converting circular structure to JSON --> starting at object with constructor 'Object' | property 'child' -> object with constructor 'Object' --- property 'parent' closes the circle

self を辿るとまた node に戻るので、終われません。

2行目から後ろは、どこで輪になったかを教えてくれる補足です。処理系によって出方が違います。

実務でいちばん多い形:親と子がお互いを持つ

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
{"a":{"n":1},"b":{"n":1}}
example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
{"name":"子"}

parentchildparent で輪になっています。 補足の行を上から読むと、どの鍵で戻ったかが分かります。

同じものが2回出るだけなら平気

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
{"name":"子","parentName":"親"}
example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
{"name":"a","self":"[循環]"}

同じものを2か所から指しているだけなら落ちません。 中身が2回書き出されるだけです。

問題になるのは自分に戻ってこられるときだけです。

直し方1: 要らない側を落とす

いちばん簡単で、たいてい正しい方法です。

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
a true false
example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
{"name":"根","children":[{"name":"子","children":[]}]}

第2引数の関数(置換関数)が、すべての鍵について呼ばれます。 undefined を返した鍵は出力から消えます。

送りたい形が決まっているなら、最初から作り直すほうが確実です。

example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
[{"id":1,"name":"根","parentId":null},{"id":2,"name":"子","parentId":1}]
javascript
const list = [1];

list.push(list);

JSON.stringify(list);
TypeError: Converting circular structure to JSON --> starting at object with constructor 'Array' --- index 1 closes the circle

「全部送って、受け取る側で選ぶ」をやめると、この問題は起きません。

直し方2: 通ったものを覚えて切る

構造が分からないものを、とりあえずログに出したいときに使います。

javascript
console.log(JSON.stringify({ m: new Map([['a', 1]]), s: new Set([1]) }));
{"m":{},"s":{}}
example.js
出力書き換えて実行できます
Ctrl+Enter でも実行JS · UTF-8
{"m":[["a",1]],"s":[1]}

落ちなくなりますが、情報は欠けます。 記録や調査のためだけに使ってください。

WeakSet を使うと、覚えたオブジェクトが後片付けの邪魔になりません。同じ理由で Set より向いています。

直し方3: 複製したいだけなら structuredClone()

JSON を経由するのが目的でないなら、そもそも JSON.stringify() は要りません。

javascript
console.log(JSON.stringify(new Error('壊れた')));
{}

structuredClone()循環をそのまま保って複製します。 別のオブジェクトになり、輪の形も残ります。

オブジェクトを結合する

直し方4: toJSON() を持たせる

そのオブジェクトを送るたびに同じ形にしたいなら、こちらです。

JSON.stringify() は、toJSON() があればその戻り値を使います。 parent は返していないので、輪になりません。

呼ぶ側は何も書かなくてよいので、忘れようがないのが利点です。 → class — 作る・継承する・隠す

そもそも輪にしない

保存や送信をするデータなら、親を持たせずに id で指すほうが素直です。

表に入る形と同じです。受け取った側で木に組み直します。 循環は「画面で使うための形」であって、送るための形ではありません。

配列でも起きる

配列のときは、鍵の名前ではなく添字が出ます。

ついでに:落ちないのに消えるもの

MapSet は、落ちませんが中身が消えます。 配列にしてから渡してください。

Map と Set

どこで起きやすいか

出どころ 何が輪になるか
親子の木構造 子が親を持っている
DOM の要素 要素が親要素・文書・窓を辿れる
通信ライブラリの応答 応答が要求を持ち、要求が応答を持つ
エラーに付けた文脈 例外に付けたオブジェクトが例外を指す

ログを送ろうとして落ちるのが、いちばんよくある出会い方です。

エラーをそのまま JSON.stringify() に渡すのも、別の理由で失敗します。

Error と try / catch / finally / throw

探しかたの順序

  1. 文言の2行目から後ろを読む。 どの鍵で戻ったかが書いてある
  2. その鍵が本当に要るか考える。たいてい要らない
  3. 要らないなら置換関数で落とすか、送る形を作り直す
  4. 中身が分からないものをとりあえず出したいなら、通ったものを覚えて切る
  5. 複製が目的なら structuredClone() に替える

このページの実行結果は Node 22.22.3 のものです。文言は処理系と版によって変わります。補足の行が出ない処理系もあります。

まとめ

  • 自分に戻ってこられる形だけが問題。同じものを2か所から指すのは平気
  • 文言の2行目から後ろに、どの鍵で輪になったかが出る
  • 直し方は要らない鍵を落とす・通ったものを覚えて切る・toJSON() を持たせる・structuredClone()
  • MapSet は落ちないが中身が消える
  • 送る形を最初から作るのがいちばん確実
  • JSON.stringify(error) は落ちないが {} になる

この記事の根拠

  1. JSON.stringify — ECMAScript® 2026 Language Specificationtc39.es
  2. SerializeJSONProperty — ECMAScript® 2026 Language Specificationtc39.es
  3. JSON.stringify() — MDNdeveloper.mozilla.org

掲載しているコード例は、公開前に読み手が押したときに動くのと同じもので実行して出力を突き合わせています。結果はリポジトリに残しています。

このページの原文(Markdown)