Что такое стек вызовов (call stack) в Node.js?

«Что такое стек вызовов (call stack) в Node.js?» — вопрос из категории Node.js Core, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Стек вызовов (call stack) в Node.js — это механизм времени выполнения, который отслеживает вызовы функций в однопоточной среде V8. Он работает по принципу LIFO (Last In, First Out — последним пришел, первым ушел). Каждый вызов функции (вызов, evaluation) помещается («заталкивается») в стек, а когда функция завершает выполнение (возвращает значение или достигает конца), она удаляется («выталкивается») из стека.

Как это работает на примере:

function greet(name) {
  return `Hello, ${name}!`;
}

function welcome() {
  const message = greet('Alice'); // (1) Вызов greet
  console.log(message); // (3) Вызов console.log
}

welcome(); // Начальный вызов

Состояние стека по шагам:

  1. welcome() помещается в стек.
  2. Внутри welcome() вызывается greet('Alice'). greet помещается поверх welcome.
  3. greet выполняется, возвращает строку и удаляется из стека.
  4. Управление возвращается в welcome. Вызывается console.log(...). console.log помещается в стек.
  5. console.log завершает работу и удаляется из стека.
  6. welcome завершает работу и удаляется из стека. Стек пуст.

Критически важная особенность Node.js: Стек вызовов работает в единственном основном потоке. Долгие синхронные операции блокируют этот стек, что приводит к «заморозке» приложения — оно не может обрабатывать новые входящие запросы или события.

Пример блокировки стека (АНТИПАТТЕР):

const fs = require('fs');

// СИНХРОННАЯ операция чтения файла — БЛОКИРУЕТ стек
const data = fs.readFileSync('/path/to/huge/file.txt'); // Стек заблокирован здесь
console.log('Это выполнится только после полного чтения файла');

Решение: Асинхронные операции и Event Loop. Асинхронные функции (например, fs.readFile, setTimeout, сетевые запросы) не выполняются в основном стеке вызовов. Они передают callback или Promise в системные API (в C++ часть Node.js/libuv), а основной поток освобождается. Когда асинхронная операция завершается, ее callback помещается в очередь обратных вызовов (callback queue), и Event Loop позже, когда стек пуст, перемещает его обратно в стек для выполнения.

// НЕБЛОКИРУЮЩИЙ асинхронный код
const fs = require('fs').promises;

async function readFile() {
  console.log('(1) Начало чтения файла (стек свободен)');
  // Асинхронный вызов — стек освобождается сразу
  const data = await fs.readFile('/path/to/file.txt');
  console.log('(3) Файл прочитан, callback выполнен');
}

console.log('(0) До вызова async функции');
readFile();
console.log('(2) Стек свободен, можно обрабатывать другие запросы');

Ошибка переполнения стека (Stack Overflow): Возникает, если глубина рекурсии становится слишком большой, например, из-за отсутствия базового случая.

function recursiveFunc() {
  recursiveFunc(); // Бесконечная рекурсия
}
recursiveFunc(); // RangeError: Maximum call stack size exceeded