Перейти к содержанию
Educora
Продвинутый22 мин20 / 27

Обработка ошибок и отладка

Больше, чем `try...catch`: встроенные типы ошибок, собственные классы ошибок с `cause`, чтение стека вызовов, `debugger` и методы консоли, которые помогают быстро находить баги.

Проверь себя
В этом уроке ты узнаешь
  • Узнавать встроенные типы ошибок (TypeError, ReferenceError, RangeError, SyntaxError) и их типичные причины
  • Создавать свои классы ошибок, добавлять cause и обрабатывать ошибки по типу, в том числе в асинхронном коде
  • Читать стек вызовов и отлаживать код с точками останова, debugger и методами консоли

Пользователь пишет: «Кнопка ничего не делает». На экране никаких признаков, но в консоли красная строка: в Chrome — TypeError: Cannot read properties of undefined (reading 'price'), а под ней имена файлов с числами. Для новичка это шум, а для профессионала — карта, которая ведёт прямо к ошибке. В этом уроке ты научишься читать такую карту и правильно проектировать собственные ошибки.

Встроенные типы ошибок

ТипТипичная причинаПример
TypeErrorоперация над значением не того типа: вызов не-функции, чтение свойства undefined/nullnull.name, (42)()
ReferenceErrorнесуществующая переменная (или переменная в TDZ)console.log(missing)
RangeErrorчисло вне допустимого диапазонаnew Array(-1)
SyntaxErrorкод или JSON, который нельзя разобратьJSON.parse('{bad}')
JavaScript
const attempts = [
  () => null.name,
  () => missingVariable,
  () => new Array(-1),
  () => JSON.parse('{bad json}'),
  () => (42)(),
];

for (const attempt of attempts) {
  try {
    attempt();
  } catch (error) {
    console.log(error.name, '|', error instanceof Error);
  }
}
▸ Ожидаемый результат
TypeError | true
ReferenceError | true
RangeError | true
SyntaxError | true
TypeError | true

Собственные ошибки и cause

throw может выбросить любое значение, но всегда выбрасывай объект Error — только у него есть стек вызовов. Хорошее правило: лови только те ошибки, которые можешь обработать, а остальные пробрасывай дальше (throw error). Чтобы различать ошибки своей программы, построй иерархию классов, наследующих Error, — тогда через instanceof можно правильно реагировать на каждый вид.

JavaScript
class AppError extends Error {
  constructor(message, options) {
    super(message, options);
    this.name = this.constructor.name;
  }
}
class ValidationError extends AppError {}
class NotFoundError extends AppError {}

function findUser(users, id) {
  if (!Number.isInteger(id)) throw new ValidationError(`Bad id: ${id}`);
  const user = users.find((u) => u.id === id);
  if (!user) throw new NotFoundError(`User ${id} not found`);
  return user;
}

const users = [{ id: 1, name: 'Aysel' }];
for (const id of [1, 'x', 7]) {
  try {
    console.log(findUser(users, id).name);
  } catch (error) {
    if (error instanceof ValidationError) console.log('400', error.message);
    else if (error instanceof NotFoundError) console.log('404', error.message);
    else throw error;
  }
}
▸ Ожидаемый результат
Aysel
400 Bad id: x
404 User 7 not found
this.constructor.name автоматически даёт каждому подклассу правильное имя. 400 и 404 — коды статуса HTTP, мы вернёмся к ним в уроке про API.

Когда ловишь низкоуровневую ошибку и выбрасываешь более понятную, сохрани исходную в свойстве **cause** (ES2022). Так пользователь видит ясное сообщение, а разработчик не теряет настоящую причину.

JavaScript
function loadSettings(text) {
  try {
    return JSON.parse(text);
  } catch (error) {
    throw new Error('Settings file is broken', { cause: error });
  }
}

try {
  loadSettings('{ theme: dark }');
} catch (error) {
  console.log(error.message);
  console.log('Caused by:', error.cause.name);
  console.log(error.cause instanceof SyntaxError);
}
▸ Ожидаемый результат
Settings file is broken
Caused by: SyntaxError
true
JavaScript
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function saveScore(score) {
  await wait(100);
  if (score < 0) throw new RangeError('Score cannot be negative');
  return 'saved';
}

try {
  console.log(await saveScore(10));
  console.log(await saveScore(-5));
} catch (error) {
  console.log(`${error.name}: ${error.message}`);
}

const result = await saveScore(-1).catch((error) => `fallback (${error.message})`);
console.log(result);
▸ Ожидаемый результат
saved
RangeError: Score cannot be negative
fallback (Score cannot be negative)

Как читать стек вызовов

Стек вызовов (stack trace) показывает цепочку вызовов в момент создания ошибки, самые новые — сверху. Первая строка — имя и сообщение ошибки, следующие строки выглядят как at функция (файл:строка:столбец). В файле cart.js ниже у второго товара нет свойства product. Вот что печатает Node.js (пути к папкам сокращены, внутренние строки Node опущены):

JavaScript
// cart.js
function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.product.price * item.qty, 0);
}
function renderCart(cart) {
  return calculateTotal(cart.items);
}
renderCart({ items: [{ product: { price: 2 }, qty: 1 }, { qty: 3 }] });
Ожидаемый результат
TypeError: Cannot read properties of undefined (reading 'price')
    at cart.js:3:57
    at Array.reduce (<anonymous>)
    at calculateTotal (cart.js:3:16)
    at renderCart (cart.js:6:10)
    at Object.<anonymous> (cart.js:8:1)

Инструменты отладки

  1. 1
    Воспроизведи ошибку

    Найди точные шаги, которые приводят к ошибке.

  2. 2
    Поставь точку останова

    В DevTools › Sources щёлкни по номеру строки или напиши в коде debugger; — при открытых DevTools выполнение остановится там.

  3. 3
    Изучи значения

    Смотри переменные в разделе Scope, наводи курсор на значения, добавляй выражения в Watch.

  4. 4
    Иди по шагам

    Двигайся по строкам, заходи внутрь функции или выходи из неё.

  5. 5
    Исправь и проверь

    После исправления снова пройди те же шаги.

открыть DevToolsF12
продолжить выполнениеF8
перейти к следующей строке (step over)F10
зайти внутрь функции (step into)F11
выйти из функции (step out)Shift+F11

Кроме log, у консоли есть и другие полезные методы. Запускатель на этой странице показывает только console.log, поэтому запусти пример в консоли браузера или в Node.js. Результат ниже — из Node.js; измеренное время для sort каждый раз разное.

JavaScript
const students = [
  { name: 'Aysel', score: 92 },
  { name: 'Murad', score: 78 },
];

console.table(students);

console.group('Checkout');
console.log('items: 3');
console.warn('coupon expired');
console.groupEnd();

console.count('click');
console.count('click');
console.assert(students.length > 5, 'Expected more than 5 students');

console.time('sort');
students.sort((a, b) => b.score - a.score);
console.timeEnd('sort');
Ожидаемый результат
┌─────────┬─────────┬───────┐
│ (index) │ name    │ score │
├─────────┼─────────┼───────┤
│ 0       │ 'Aysel' │ 92    │
│ 1       │ 'Murad' │ 78    │
└─────────┴─────────┴───────┘
Checkout
  items: 3
  coupon expired
click: 1
click: 2
Assertion failed: Expected more than 5 students
sort: 0.027ms
Задание

Допиши класс ValidationError (его name должен быть 'ValidationError') и напиши parseAge(text). Если текст — не целое число, выброси ValidationError с сообщением Not a whole number: <text>, если вне диапазона 0–120 — с сообщением Out of range: <age>, иначе верни возраст.

Задание · JavaScript
class ValidationError extends Error {
  // set this.name in the constructor
}

function parseAge(text) {
  // Number(text) must be an integer from 0 to 120
}

for (const input of ['16', 'abc', '130', '42']) {
  try {
    console.log('Age:', parseAge(input));
  } catch (error) {
    if (error instanceof ValidationError) {
      console.log(`${error.name}: ${error.message}`);
    } else {
      throw error;
    }
  }
}
▸ Ожидаемый результат
Age: 16
ValidationError: Not a whole number: abc
ValidationError: Out of range: 130
Age: 42
Задание

Напиши retry(fn, times): она вызывает fn не больше times раз, пока вызов не будет успешным, и после каждой неудачи выводит Retrying after: <сообщение>. Если все попытки неудачны, выброси ошибку с сообщением All <times> attempts failed, которая хранит последнюю ошибку в cause.

Задание · JavaScript
function createFlakyTask(failuresBeforeSuccess) {
  let calls = 0;
  return () => {
    calls++;
    if (calls <= failuresBeforeSuccess) throw new Error(`Attempt ${calls} failed`);
    return `Success on attempt ${calls}`;
  };
}

function retry(fn, times) {
  // your code here
}

console.log(retry(createFlakyTask(2), 3));
try {
  retry(createFlakyTask(5), 3);
} catch (error) {
  console.log(error.message, '| cause:', error.cause.message);
}
▸ Ожидаемый результат
Retrying after: Attempt 1 failed
Retrying after: Attempt 2 failed
Success on attempt 3
Retrying after: Attempt 1 failed
Retrying after: Attempt 2 failed
Retrying after: Attempt 3 failed
All 3 attempts failed | cause: Attempt 3 failed

Главное

  • TypeError — не тот тип или undefined, ReferenceError — неизвестная переменная, RangeError — значение вне диапазона, SyntaxError — код или JSON, который нельзя разобрать.
  • Проверяй error.name или instanceof, а не текст сообщения — сообщения зависят от браузера.
  • Собственные классы ошибок и cause делают ошибки осмысленными и сохраняют исходную причину.
  • Асинхронная ошибка ловится только через await внутри try или через .catch().
  • Читай стек с первой строки своего файла; вместо догадок используй точки останова, debugger и методы консоли.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Какую ошибку выбросит const user = null; user.length;?