
Любое объяснение этой ошибки говорит вам одно и то же, правдивое и бесполезное: функция вызвала саму себя слишком много раз, так что добавьте базовый случай.
Это вы и так знали. Вы хотите знать, какой вызов это начал, и это единственная ошибка в 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. Один клик на странице, которая ведёт себя не так, как надо, захватывает скриншот, консоль и сетевой журнал и выдаёт вам ссылку, которую можно вставить в тикет.
Что из этого забрать
Ошибка честная и почти бессодержательная: что-то рекурсировало без остановки. Она называет функцию в основании цикла и умалчивает о той, что наверху, не из скупости, а из-за места, откуда снимается стек вызовов.
Так что перестаньте читать этот стек и идите за другим, с раннего прохода, с поднятым лимитом. Или, когда это чужой браузер, перестаньте просить консоль и спросите, что человек нажал.