スタックには上限があります。上限を超えるまで積み続けると、RangeError: Maximum call stack size exceeded が投げられます。これは通常の例外なので try/catch で捕まえられ、投げられた時点の深さも数えられます。

// depth.js
let depth = 0;
function recurse() {
  depth++;
  recurse();
}

try {
  recurse();
} catch (e) {
  console.log(e.name + ': ' + e.message);
  console.log('depth', depth);
}

この 9,183 という数は、PC のスペックではなく、V8 のオプション --stack-size(既定 864 KB)で決まります。--stack-size を変えると深さも変わります。

--stack-size を上げれば深くなりますが、OS 側のスタック上限(ulimit -s)以上にすると、例外にもならず、プロセスごと終了コード 1 で終わりました。終了コード 1 の中身までは調べていません。V8 の値と OS の値の両方が上限を決めていて、OS 側を上げれば同じ --stack-size でも通ります。

もう 1 つ、深さは「回数」ではなく「バイト数」で決まります。1 つのフレームが大きい(引数が多い)と、同じ上限でも浅くなります。引数 6 個の関数で同じ計測をします。

同じ上限でも、フレームが大きいほど積める数は減る

// depth6.js
let depth = 0;
function recurse(a, b, c, d, e, f) {
  depth++;
  recurse(a, b, c, d, e, f);
}

try {
  recurse(1, 2, 3, 4, 5, 6);
} catch (e) {
  console.log('depth', depth);
}

引数なしの 9,183 に対して 4,591 で、ほぼ半分です。

同じ既定値でも、Node のバージョンで深さは違います。depth.js と depth6.js を各バージョンで 2 回ずつ実行し、どちらも同じ値でした。

v22 だけ浅く、v24 で少し戻っています。1 フレームの大きさが変わったのか、V8 の実装が変わったのかは調べていません。また、測ったのは macOS だけで、Linux・Windows では別の値になるかもしれません。「およそ 1 万回」を上限の目安にはできても、バージョンをまたいで同じ値を当てにはできません。