Любое объяснение этой ошибки говорит вам одно и то же, правдивое и бесполезное: функция вызвала саму себя слишком много раз, так что добавьте базовый случай.

Это вы и так знали. Вы хотите знать, какой вызов это начал, и это единственная ошибка в JavaScript, чей стек вызовов структурно не может вам этого сказать.

Всё измеренное ниже выполнялось в Chrome. Числа принадлежат V8; сама форма сохраняется в любом движке, который снимает стек в момент выброса.

Что на самом деле лежит в стеке

Возьмите цепочку, где одна функция вызывает другую, та третью, а третья начинает рекурсию, и поймайте ошибку:

function entryPoint() { return level1() }
function level1()     { return level2() }
function level2()     { return recurse(0) }
function recurse(n)   { return recurse(n + 1) }

try { entryPoint() } catch (e) { console.log(e.stack) }

RangeError: Maximum call stack size exceeded, а затем десять строк, и каждая из них recurse:

RangeError: Maximum call stack size exceeded
    at recurse (<anonymous>:2:145)
    at recurse (<anonymous>:2:156)
    at recurse (<anonymous>:2:156)
    ... seven more identical lines

entryPoint здесь нет. Нет и level1 с level2. Стек называет десять раз ту функцию, о которой вы уже знали, и больше ничего.

Десять, потому что Error.stackTraceLimit по умолчанию равен 10.

Поднять лимит не помогает, и это та часть, которую стоит знать

Очевидный ход это попросить больше кадров:

Error.stackTraceLimit = 200;

Сделайте так, и вы получите 200 строк. На цепочке выше все 200 оказываются recurse, а entryPoint по-прежнему отсутствует.

Причина в том, откуда снимается стек. Он берётся в момент выброса, то есть в самой глубокой точке стека, и отматывается оттуда назад. Ваша точка входа находится на другом конце, в тысячах кадров. Запросив 200 кадров, вы получите 200 кадров не с того конца.

Поэтому здесь не работает правило “читать стек снизу вверх”, которое верно почти для любой другой ошибки. Низ этого стека это тоже recurse.

Снимите его рано, и окно окажется узким

Нужная вам информация существует. Она доступна только пока стек ещё неглубокий.

Поэтому снимайте стек изнутри рекурсирующей функции, на раннем проходе, а саму ошибку выбрасывайте:

function recurse(n) {
  if (n === 2) console.log(new Error("where did this start").stack);
  return recurse(n + 1);
}

На цепочке выше такой стек заканчивается так:

    at level2 (<anonymous>:4:27)
    at level1 (<anonymous>:4:27)
    at entryPoint (<anonymous>:3:31)

И это ответ. Вся цепочка, по порядку, начиная с вызова, который её начал.

Окно примерно такой же ширины, как ваш лимит кадров, и в этом подвох. Снимок на глубине 2 даёт точку входа. Снимок на глубине 20, при лимите, оставленном на значении 10 по умолчанию, её уже не даёт, потому что к этому моменту те десять кадров, которые стек сохранит, целиком ушли за начало. То есть два приёма работают вместе, и ни один не работает в одиночку: поднимите Error.stackTraceLimit и снимайте на раннем проходе.

Если вы не знаете, какая функция рекурсирует, хотя бы это ошибка вам говорит. Это имя, повторённое десять раз.

Та версия, которая приходит в тикете

Всё описанное выше предполагает, что вы за клавиатурой, пока это происходит. Подумайте, что приходит к вам, когда это происходит с кем-то другим.

Страница зависла, или побелела, или кнопка перестала отвечать. Если человек открыл консоль и сфотографировал её, у вас есть десять одинаковых строк с именем функции. На этом изображении нет никакой информации о том, какой путь кода выполнялся, а это единственное, что вам нужно.

Это делает переполнение стека необычным среди браузерных ошибок. Для большинства сбоев консоль это тот артефакт, о котором стоит просить, а для этой она почти бесполезна: десять копий факта, который можно было угадать. Сужает круг на самом деле последовательность действий прямо перед зависанием, потому что точка входа это то, что сделал человек.

И это неудобная инверсия. Ошибка с наибольшим выводом в консоль это та ошибка, где вывод в консоль помогает меньше всего и где то, что человек делал, значит больше всего.

Session Replay

Бесплатное расширение для Chrome. Один клик на странице, которая ведёт себя не так, как надо, захватывает скриншот, консоль и сетевой журнал и выдаёт вам ссылку, которую можно вставить в тикет.

Получить расширение

Что из этого забрать

Ошибка честная и почти бессодержательная: что-то рекурсировало без остановки. Она называет функцию в основании цикла и умалчивает о той, что наверху, не из скупости, а из-за места, откуда снимается стек вызовов.

Так что перестаньте читать этот стек и идите за другим, с раннего прохода, с поднятым лимитом. Или, когда это чужой браузер, перестаньте просить консоль и спросите, что человек нажал.