Что происходит при запуске бесконечной рекурсивной функции с console.log в браузере
Что проверяют
Знаете ли вы, что стек вызовов конечен, и отличаете ли ситуацию «движок бросил ошибку» от «браузер умер». Попутно смотрят, различаете ли вы синхронную рекурсию и рекурсию через очередь задач: это два совсем разных сценария.
Как отвечать
Каждый вызов кладёт в стек новый кадр с аргументами и локальными переменными. Стек ограничен, поэтому через несколько тысяч вложенных вызовов движок бросает RangeError с текстом про превышение максимального размера стека (формулировка у каждого движка своя). Это обычная ошибка, её ловит try/catch, стек при этом разматывается, и страница продолжает жить.
function boom() { console.log("шаг"); boom(); }
try { boom(); } catch (e) { console.log(e instanceof RangeError); } // trueПока цепочка вызовов идёт, поток один и он занят: вкладка не рисует кадры и не обрабатывает клики. С console.log это ощутимо дольше, потому что каждый вызов синхронно пишет в консоль, а при открытых инструментах разработчика записи ещё и удерживаются в памяти вместе со ссылками на аргументы. Отсюда картина «вкладка повисла»: дело не в рекурсии как таковой, а в выводе.
Отдельно стоит сказать, что предельная глубина не описана в стандарте: она зависит от движка и от размера кадра, то есть от числа аргументов и локальных переменных. Лечится переписыванием в цикл с явным стеком или разбиением работы на порции.
Где спотыкаются
Отвечают «браузер зависнет насмерть». На деле движок бросает RangeError, освобождает стек и работает дальше; зависание, если оно случается, это про залитую консоль и память, а не про само переполнение.
Второе: рассчитывают на оптимизацию хвостовых вызовов. В спецификации она описана, но в большинстве движков не реализована, поэтому приём «поставлю вызов последним выражением» стек не спасает.
Третье: не отличают рекурсию через setTimeout или await. Там каждый шаг начинается с пустым стеком, переполнения не будет никогда, зато получится бесконечная очередь задач: страница отзывается, память растёт, а try/catch вокруг первого вызова ничего не поймает.