Async stack trace
При асинхронных вызовах синхронный стек теряется — между двумя await-ами стек "обнуляется", и в обычной ситуации
Error.stackне показывает, откуда async-функция была вызвана. В V8 (Node 12 / V8 7.2+) проблему починили--async-stack-traces.
Что это / Зачем
Когда асинхронная функция возобновляется после await или коллбэк вызывается из event loop, его call stack начинается «с нуля» — все верхние фреймы уже сняты. Без специальной поддержки рантайма ошибка содержит только локальный стек, а связь с вызывающим кодом теряется.
Это критично для отладки: видишь ENOENT где-то в глубине, но не знаешь, какой пользовательский код это запустил.
API / Синтаксис
// Симптом: ошибка не попадает в catch
const cause = () => { throw new Error('boom'); };
const f = async () => {
setTimeout(cause, 0); // throw полетит из event loop, await его не поймает
};
try { await f; }
catch (e) { console.log('catch'); } // НЕ выполнится
// async stack traces — встроены с Node 12
// node --async-stack-traces script.js (в Node 12+ включено по умолчанию)
async function step1() { await step2; }
async function step2() { await Promise.reject(new Error('oops')); }
try { await step1; }
catch (e) { console.log(e.stack); }
// Видно цепочку: step2 → step1 → top-level (благодаря async stack traces)
Ключевые моменты
- В коллбэк-стиле (
setTimeout(cb),fs.readFile(path, cb)) стек теряется ВСЕГДА —Error.stackвнутри cb не содержит вызвавшего кода. - В async/await V8 хранит "logical stack" между
await— после возобновления он собирается обратно. - Флаг V8
--async-stack-traces(Node 12+, включён по умолчанию) расширяет стек цепочкой await-ов. - Throw из чистого callback'а не превращается в reject —
try/catchснаружи async-функции его не поймает. - Throw из async-функции возвращается через её Promise → ловится
try/catchс await или.catch.
Подводные камни
- Если используете callback-стиль (legacy lib) — стек всё ещё теряется, async stack traces не помогают.
- Библиотеки вроде longjohn / trace-and-clarify искусственно склеивают трейсы — даёт overhead, в проде включать не стоит.
- Реальная цепочка может быть длинной — V8 ограничивает глубину async stack по умолчанию ~10 фреймов.
- В Node 11 и младше — старый V8 без async stack traces, отлаживать тяжело.
🎓 Источники
- 🎓 [Проблема асинхронного стектрейса в JavaScript и Node.js] · 2019-03-21 · YouTube
- Тезисы:
- До Node 12 / V8 7.2 async-цепочка в стеке полностью терялась.
- Throw из callback не попадает в
try/catchснаружи — оборачивается в Unhandled Rejection в лучшем случае. - async-функции дают видимый stack даже без таймеров (поскольку throw превращается в reject Promise).
- С таймером (callback-style) стек всё равно не восстанавливается.
- Сторонние библиотеки склеивают стеки за счёт overhead'а.
- Цитата:
«Один из самых неприятных моментов асинхронного программирования — потеря стека вызовов. Между двумя await-ами стек по сути обнуляется.»
- Тезисы: