Как дебажить ошибки async/await в TypeScript?

Я работаю над Node.js API на TypeScript и уже пару недель мучаюсь с async/await. Промисы как-то неправильно разрешаются, и я получаю криптические сообщения об ошибках, которые мне не особо помогают. Как лучше всего подходить к отладке в таких случаях?

5 ответов

★ Лучший ответ

Проблема обычно не в самом async/await, а в том, что ты ошибки вообще не видишь - потому что они приземляются в неправильном scope или потому что Promise в фоне молча reject'ится.

Со мной недавно было так же: была функция, которая делала несколько API-запросов параллельно, и один из них упал. Я сначала подумал, что мой await-код сломан, но в конце оказалось, что я просто не залогировал ошибку. Поэтому: пиши `.catch()` везде, даже на Promise'ы которые тебе «не нужны», и логируй агрессивно через `console.error()` или логгер - дай ошибке полный контекст. Это помогает намного больше, чем читать Stack Trace'ы. И всегда проверяй, не используешь ли ты где-нибудь Promise.all() или Promise.race() - они тоже могут тебя подвести, если Promise тихо упадёт.

"Криптические сообщения об ошибках" обычно вовсе не криптические - ты просто должен знать, где смотреть. Напиши свои async функции с явными блоками `.catch()` вместо просто try-catch и логируй весь Error-Stack, не только сообщение. Используй также Node.js Debugger или Chrome DevTools, чтобы проходить пошагово через свои Promises - тогда ты сразу увидишь, где Promise зависает или вообще не разрешается. Частая ошибка - забыть `await` или случайно запустить Promises без `await`; с помощью `eslint-plugin-promise` ты автоматически поймаешь такие вещи.

При отладке async/await в TypeScript stack trace - твой лучший друг, но нужно уметь его читать. Если Promise висит неразрешённым, сначала проверь, не забыл ли ты где-то `await`. Это происходит чаще, чем кажется: ты вызываешь async функцию, но не ждёшь результат. То же самое с `.map()` или `.forEach()` - часто там всё ломается, потому что callbacks асинхронные, а сами методы не ждут. Здесь спасают `Promise.all()` или `for...of` вместо методов массива.

Второй важный момент - посмотри на свою стратегию обработки ошибок. Если у тебя только try-catch и error message всё ещё выглядит криптичным, логируй весь error object, а не только message. `console.log(error)` вместо `console.log(error.message)` часто показывает намного больше контекста. И самое главное - в Node.js есть unhandled promise rejections, которые просто тихо умирают в фоне. Включи listener-event `process.on('unhandledRejection', ...)` при отладке, и ты увидишь, где promises действительно падают.

Практический совет: используй сам TypeScript как инструмент отладки. Если твои type annotations правильные, TypeScript compiler скажет тебе, если ты забыл await - получишь type error вместо runtime-сюрприза. Это экономит время на отладку.

один полезный трик, который я использую: логируй время и контекст перед и после каждого await. так сразу видишь, где что зависает или вечно грузится. `console.time('myFunc')` перед async функцией, потом `console.timeEnd('myFunc')` после. звучит просто, но часто сразу показывает, где узкое место - иногда это зависимости, которые вечно грузятся, а не твой код.

ещё важнее: используй `Promise.all()` или `Promise.allSettled()`, когда запускаешь несколько промисов параллельно. `allSettled` - золото, потому что видишь, какие промисы упали и какие нет - вместо того, чтобы один упал рано и остальные были проигнорированы. с `allSettled` ты получаешь для каждого промиса объект со статусом и value/reason, там сразу видно, где зацепка.

и один хак, который многие упускают: добавь в твой tsconfig.json `"lib": ["es2020"]` (или новее) и правильно используй optional chaining и nullish coalescing с результатами промисов. но честно говоря - напиши также `debugger;` statements прямо в async функции и запусти ноду с `--inspect`. потом откроешь chrome://inspect и отлаживаешь в реальном времени. там видишь полный call stack и можешь пошагово смотреть, где зависают промисы. правда, это более затратно, чем логи, но если логи тебе не помогают, это правильный ход.

Забудь про все эти `.catch()`-блоки рядом с try-catch, это только запутывает - придерживайся одной стратегии. Что действительно помогает: включи `unhandledRejection`-события в своём Node приложении (`process.on('unhandledRejection', ...)`) и логируй там всё, иначе твои Promise'ы умрут беззвучно. Потом напиши временно простое wrapper-утилиту, которая обвивает каждую async функцию логированием - перед await записываешь timestamp и имя функции, после - снова, и сразу видишь, если что-то зависает или работает слишком долго. `console.time()` работает, но это слишком детально для больших функций; несколько стратегически расставленных `console.log('перед await X')` на начальном этапе полезнее.

Ваш ответ

Войдите, чтобы ответить.