Отладка — это проверка гипотез, а не перебор правок наугад: воспроизвести ошибку, сузить место, где она возникает, и подтвердить причину фактами. Stack trace читают сверху вниз: первая строка — тип и текст ошибки, первая строка at с вашим файлом — место, где программа упала. Когда исключения нет, а результат неверный, ставят breakpoint и смотрят значения переменных в момент выполнения — в Chrome DevTools, VS Code или через node --inspect.
После материала вы сможете
- прочитать stack trace Node.js или браузера до первой строки своего кода и назвать файл, строку и функцию
- найти баг без исключения через console.table и проверку типов значений
- поставить breakpoint, посмотреть стек вызовов и значения переменных в Chrome DevTools, VS Code или node inspect
- сформулировать гипотезу о причине бага и подтвердить её до исправления
Что знать заранее
Проверено на
Node.js 24 LTS (24.18)Chrome DevToolsVS Code
Сверено с документацией: 11 октября 2026 г.
Содержание
Отладка как метод: воспроизвести, сузить, проверить
Самая дорогая привычка начинающего — менять код наугад, пока ошибка не исчезнет. Иногда она исчезает, но причина остаётся и возвращается в другом месте. Отладка (debugging) — это короткий цикл из четырёх шагов, который работает на любом языке.
Воспроизвести
Найти минимальные действия или данные, при которых ошибка повторяется каждый раз. Ошибку, которую нельзя повторить, нельзя и проверить.
Сузить
Определить, в каком файле, функции и строке значения становятся неправильными. Здесь помогают stack trace, console и breakpoints.
Сформулировать гипотезу
Одно проверяемое предложение: «доставка приходит строкой, и + склеивает строки вместо сложения».
Проверить и исправить
Подтвердить гипотезу фактом — выводом в консоль или значением в отладчике — и только потом менять код. После правки повторить шаг 1.
Разберём цикл на учебной программе: она считает сумму заказа в кофейне с доставкой и промокодом. В ней три намеренных бага разного вида: падение с исключением, неверный результат из-за типов и условие, которое никогда не срабатывает.
export function calcSubtotal(items) {
let total = 0;
for (let i = 0; i <= items.length; i++) {
total += items[i].price * items[i].qty;
}
return total;
}
export function addDelivery(subtotal, delivery) {
return subtotal + delivery;
}
export function applyPromo(total, code) {
if (code === "sale") return Math.round(total * 0.9);
return total;
}
export function checkout(cart) {
const subtotal = calcSubtotal(cart.items);
const withDelivery = addDelivery(subtotal, cart.delivery);
return applyPromo(withDelivery, cart.promo);
}import { checkout } from "./cart.mjs";
const cart = {
items: [
{ name: "latte", price: 1500, qty: 2 },
{ name: "croissant", price: 1200, qty: 1 },
],
delivery: "500",
promo: "SALE",
};
console.log("Total:", checkout(cart));Баг 1: читаем stack trace сверху вниз
Запускаем node main.mjs и получаем падение. Вывод — реальный, с Node.js 24; длинный путь к папке заменён на …:
$ node main.mjs
file:///…/cart.mjs:4
total += items[i].price * items[i].qty;
^
TypeError: Cannot read properties of undefined (reading 'price')
at calcSubtotal (file:///…/cart.mjs:4:23)
at checkout (file:///…/cart.mjs:19:20)
at file:///…/main.mjs:12:23
at ModuleJob.run (node:internal/modules/esm/module_job:439:25)
at async node:internal/modules/esm/loader:643:26
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
Node.js v24.18.0cart.mjs:4 и строка с ^
- Что она говорит
- Node.js показывает исходную строку и ставит знак
^под местом ошибки
TypeError: Cannot read properties of undefined (reading 'price')
- Что она говорит
- Тип и текст: у значения
undefinedпопытались прочитать свойствоprice
at calcSubtotal (…/cart.mjs:4:23)
- Что она говорит
- Самый свежий вызов: функция, файл, строка 4, колонка 23. Отсюда начинаем
at checkout (…/cart.mjs:19:20)
- Что она говорит
- Кто вызвал
calcSubtotal
at file:///…/main.mjs:12:23
- Что она говорит
- Кто вызвал
checkout: верхний уровень модуля, поэтому без имени функции
at ModuleJob.run (node:internal/…)
- Что она говорит
- Внутренний код Node.js — обычно пропускаем
По документации Node.js первая строка стека имеет вид «класс ошибки: сообщение», а дальше идут кадры (frames), каждый начинается с at. Самый свежий вызов наверху, точка входа — внизу. Правило чтения: найдите первую строку at с вашим файлом, а не с node:internal или node_modules. Это место, где сломалось, хотя причина может быть выше по стеку — в том, кто передал плохие данные.
Гипотеза: на какой-то итерации items[i] равен undefined. В массиве два элемента с индексами 0 и 1, а условие i <= items.length пускает цикл и на индекс 2. Это классическая ошибка «на единицу» (off-by-one). Исправление — строгое <.
Баг 2: когда исключения нет — console.log и console.table
После исправления цикла программа выводит Total: 4200500. Исключения нет, но результат абсурдный. Stack trace не появится — сужать придётся самим. Самый быстрый инструмент — временный вывод значений прямо перед подозрительным местом. console.table показывает массив объектов таблицей, а typeof — тип значения:
console.table(cart.items);
console.log("delivery:", cart.delivery, typeof cart.delivery);$ node main.mjs
┌─────────┬─────────────┬───────┬─────┐
│ (index) │ name │ price │ qty │
├─────────┼─────────────┼───────┼─────┤
│ 0 │ 'latte' │ 1500 │ 2 │
│ 1 │ 'croissant' │ 1200 │ 1 │
└─────────┴─────────────┴───────┴─────┘
delivery: 500 string
Total: 4200500Гипотеза подтверждена: delivery — строка "500", и оператор + склеивает 4200 и "500" в "4200500". Такие данные часто приходят из форм, query-параметров и JSON, где число записано в кавычках. Исправляем на границе — приводим к числу там, где значение входит в расчёт:
--- a/cart.mjs
+++ b/cart.mjs
@@ -1,11 +1,11 @@
export function calcSubtotal(items) {
let total = 0;
- for (let i = 0; i <= items.length; i++) {
+ for (let i = 0; i < items.length; i++) {
total += items[i].price * items[i].qty;
}
return total;
}
export function addDelivery(subtotal, delivery) {
- return subtotal + delivery;
+ return subtotal + Number(delivery);
}$ node main.mjs
Total: 4700Баг 3: breakpoint, стек вызовов и watch
Итог 4700 — без скидки. Ошибки нет, все типы числа. Нужно остановить программу внутри applyPromo и посмотреть, что в ней происходит. Breakpoint (точка останова) — место, где отладчик ставит выполнение на паузу до выполнения строки. Самый простой способ — инструкция debugger;: она работает как breakpoint, если подключён отладчик, и ничего не делает без него.
Добавим debugger; первой строкой applyPromo и запустим встроенный консольный отладчик Node.js — node inspect. Ниже реальная сессия: cont — продолжить до следующей остановки, bt — стек вызовов, exec — вычислить выражение, watch — следить за значением, next — шаг на следующую строку.
$ node inspect main.mjs
< Debugger listening on ws://127.0.0.1:9229/31bee525-ecb9-4a6c-b9ea-f75545b276bf
< For help, see: https://nodejs.org/learn/getting-started/debugging
<
connecting to 127.0.0.1:9229 ... ok
< Debugger attached.
<
debug> Break on start in cart.mjs:1
> 1 export function calcSubtotal(items) {
2 let total = 0;
3 for (let i = 0; i < items.length; i++) {
debug> cont
debug> break in cart.mjs:14
12
13 export function applyPromo(total, code) {
>14 debugger;
15 if (code === "sale") return Math.round(total * 0.9);
16 return total;
debug> bt
#0 applyPromo cart.mjs:14:2
#1 checkout cart.mjs:22:9
#2 (anonymous) main.mjs:12:22
#3 run node:internal/modules/esm/module_job:439:24
debug> exec code
'SALE'
debug> exec code === "sale"
false
debug> watch('total')
debug> next
debug> step in cart.mjs:15
Watchers:
0: total = 4700
13 export function applyPromo(total, code) {
14 debugger;
>15 if (code === "sale") return Math.round(total * 0.9);
16 return total;
17 }
debug> cont
debug> < Total: 4700
<
debug> < Waiting for the debugger to disconnect...
<
debug> .exitФакты из отладчика: code равен 'SALE', а сравнение с "sale" даёт false — регистр не совпадает. Стек bt показывает ту же цепочку, что и stack trace: applyPromo ← checkout ← main.mjs. Исправляем сравнение и убираем debugger;:
- if (code === "sale") return Math.round(total * 0.9);
+ if (code?.trim().toLowerCase() === "sale") return Math.round(total * 0.9);$ node main.mjs
Total: 4230Тот же поиск в DevTools и VS Code
Консольный отладчик полезен, чтобы понять механику, но в работе удобнее графический. Под капотом он тот же: Node.js с флагом --inspect открывает порт для отладчика, и к нему подключаются Chrome DevTools или VS Code. По умолчанию это 127.0.0.1:9229. Флаг --inspect-brk дополнительно останавливает программу до первой строки вашего кода, чтобы успеть расставить breakpoints:
$ node --inspect-brk main.mjs
Debugger listening on ws://127.0.0.1:9229/5f699728-1ccb-4e05-ba8a-2afa59a35332
For help, see: https://nodejs.org/learn/getting-started/debuggingПодключиться к Node.js
- Chrome DevTools
- Открыть
chrome://inspect(в Edge —edge://inspect), процесс появится в Remote Target - VS Code
- Терминал JavaScript Debug Terminal: любой
nodeв нём отлаживается автоматически
Отладка кода страницы
- Chrome DevTools
- Панель Sources, файл скрипта
- VS Code
- Через конфигурацию запуска браузера в launch.json
Поставить breakpoint
- Chrome DevTools
- Клик по номеру строки в Sources
- VS Code
- Клик слева от номера строки или F9
Условная остановка
- Chrome DevTools
- Правый клик по номеру строки → Add conditional breakpoint
- VS Code
- Правый клик на полях → Add Conditional Breakpoint
Вывести без остановки
- Chrome DevTools
- Logpoint: сообщение в Console без паузы
- VS Code
- Logpoint: выражения в фигурных скобках, например
{code}
Шаги
- Chrome DevTools
- Resume (F8), шаг по инструкции (F9), Step over / into / out — кнопками
- VS Code
- Continue F5, Step Over F10, Step Into F11, Step Out Shift+F11
Смотреть значения
- Chrome DevTools
- Панели Scope, Watch и Call Stack
- VS Code
- Разделы Variables, Watch и Call Stack
- Остановка на исключениях: в DevTools можно включить паузу на перехваченных и неперехваченных исключениях — отладчик остановится там, где ошибка брошена, со всеми переменными.
- Restart frame в DevTools перезапускает текущую функцию без перезапуска всей программы — удобно, чтобы пройти её ещё раз после изменения значения в Scope.
- Условный breakpoint вроде
i === items.lengthв цикле из бага 1 остановит программу ровно на проблемной итерации, а не на каждой.
Чек-лист перед тем, как написать «не работает»
Если ошибка в веб-приложении связана с данными с сервера, начните с вкладки Network в DevTools: статус-код, тело запроса и ответа часто сразу показывают, на чьей стороне проблема. Подробно о запросах, статусах и JSON — в уроке «HTTP и JSON».
- Ошибку можно воспроизвести, и вы записали точные шаги или данные
- Прочитан полный текст ошибки, а не только её тип
- Найдена первая строка stack trace с вашим файлом: файл, строка, функция
- Проверены типы и значения на входе подозрительной функции
- Для запроса к API посмотрены метод, статус-код и тело ответа в Network
- Гипотеза подтверждена фактом до исправления
- Временные console.log и debugger удалены из кода
- В сообщении коллеге есть шаги, ожидаемый и фактический результат и stack trace
Как проверено7 · пройдено1 · не запускалосьсверено 11 октября 2026 г.
- запущено, работает
cart.mjs + main.mjs (три бага) и stack trace бага 1
node main.mjs на Node.js 24.18.0 (Windows 11); вывод вставлен реальный, путь к папке scratchpad заменён на «…» (sed), остальное без правок
- запущено, работает
console.table / typeof (баг 2)
фрагмент добавлен в копию main.mjs после правки бага 1; вывод реальный
- запущено, работает
Исправления (diff) багов 1–3
правки применены sed в копиях step2/step3/fixed; выводы 4200500 → 4700 → 4230 реальные
- запущено, работает
Сессия node inspect (debugger; cont, bt, exec, watch, next)
node inspect main.mjs, команды подавались скриптом drive-inspect.mjs через stdin с паузой 1,5 с; транскрипт вставлен как есть (UUID — из запуска)
- запущено, работает
node --inspect-brk main.mjs
запуск с timeout 4 с, вывод реальный; подключение Chrome DevTools к процессу не выполнялось
- запущено, работает
Упражнение repeat: node inspect + breakOnException
реальная сессия на исходной версии с багом 1: cont без breakpoint — программа завершается; breakOnException — пауза на cart.mjs:4, i = 2, items.length = 2
- запущено, работает
Упражнение «Пустая корзина»: empty-cart.mjs и проверка в checkout
запуск до и после добавления проверки, выводы реальные (путь заменён на «…»)
- не запускалось
Шаги в Chrome DevTools и VS Code (таблица, упражнение modify)
UI-шаги описаны по официальной документации Chrome DevTools и VS Code; вручную в браузере и редакторе не прокликивались
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.