Обработка ошибок в JavaScript: try/catch и throw

Содержание

О проверке кода. Примеры не запускались — в отличие от материалов по Node.js с фактическим выводом. Говорим прямо, а не ставим бейдж «проверено».

Ошибки в программе неизбежны: сеть отвалилась, данные не те, файла нет. Вопрос не в том, будут ли ошибки, а в том, как программа на них реагирует. Разберём инструменты JavaScript для этого.

try/catch: основа

try {
  const data = JSON.parse(input);   // может бросить, если input не JSON
  console.log(data.name);
} catch (error) {
  console.error('Не разобрать JSON:', error.message);
}

Код в try выполняется как обычно. Если внутри возникает ошибка, выполнение try немедленно прерывается, и управление переходит в catch с объектом ошибки. Если ошибки нет — catch пропускается целиком.

Важно: try/catch ловит только синхронные ошибки внутри блока. Ошибка в отложенном коде (setTimeout, колбэк) сюда не попадёт — про асинхронность ниже.

throw: бросить ошибку

function divide(a, b) {
  if (b === 0) {
    throw new Error('Деление на ноль');
  }
  return a / b;
}

try {
  divide(10, 0);
} catch (e) {
  console.log(e.message);   // "Деление на ноль"
}

throw останавливает функцию и передаёт ошибку вверх по стеку вызовов, пока её не поймает ближайший catch. Если не поймает никто — программа падает (в браузере — ошибка в консоли, в Node.js — краш процесса).

Бросайте объект Error, а не строку:

throw new Error('текст');   // ✅ есть message, name, stack
throw 'текст';              // ❌ нет стека вызовов, ломает единообразие

У Error есть stack — стек вызовов, показывающий, где именно всё сломалось. throw 'строка' этого лишает и заставляет catch гадать, что прилетело. Единообразие важнее краткости.

finally: выполнится всегда

function readData(file) {
  const handle = open(file);
  try {
    return parse(handle);      // даже с return...
  } catch (e) {
    return null;
  } finally {
    handle.close();            // ...finally всё равно закроет файл
  }
}
Порядок выполнения try/catch/finally
  1. 1 tryВыполняется код. Если всё ок — сразу к finally
  2. 2 Ошибка?Если бросок — прыжок в catch с объектом ошибки
  3. 3 catchОбработка: логируем, восстанавливаемся или пробрасываем
  4. 4 finallyВыполняется ВСЕГДА — очистка ресурсов

finally выполняется в любом случае — при успехе, при ошибке, даже если в try или catch был return. Сюда кладут освобождение ресурсов: закрыть файл, соединение, убрать спиннер загрузки. Это гарантия, что уборка произойдёт независимо от исхода.

Свои классы ошибок

Для разных ситуаций полезно различать типы ошибок:

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
  }
}

try {
  throw new ValidationError('Пустое имя', 'name');
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(`Поле ${e.field}: ${e.message}`);   // осмысленная реакция
  } else {
    throw e;   // не наша ошибка — пробрасываем дальше
  }
}

Свой класс ошибки (наследник Error) позволяет отличать ожидаемые ошибки (валидация, «не найдено») от неожиданных и реагировать по-разному. Проверка через instanceof — стандартный приём. Чужие ошибки, которые вы не умеете обрабатывать, пробрасывайте дальше через throw e.

Ошибки в async/await

Асинхронный код ловят тем же try/catch:

async function loadUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (e) {
    console.error('Не загрузить пользователя:', e.message);
    return null;
  }
}

Отклонённый промис внутри await превращается в исключение, которое ловит обычный catch. Это главное удобство async/await по сравнению с .catch() у промисов: обработка ошибок выглядит как в синхронном коде.

Тонкость: fetch не бросает на HTTP-ошибках вроде 404 — только на сетевых сбоях. Статус проверяют вручную через res.ok, как в примере.

Антипаттерны

Как НЕ надо обрабатывать ошибки

❌ Так делать нельзя

Прячет проблемы

  • Пустой catch {} — глотает ошибку молча
  • catch (e) {} без логирования
  • throw 'строка' — без стека
  • Ловить всё подряд и игнорировать

✅ Правильно

Ошибка видима и обработана

  • Обработать осмысленно
  • Или залогировать
  • Или пробросить дальше throw e
  • Различать типы через instanceof

Худшее, что можно сделать с ошибкой — проглотить её. Пустой catch {} оставляет программу в сломанном состоянии, а вы не узнаете о баге, пока пользователь не пожалуется. С ошибкой делают одно из трёх: осмысленно обрабатывают, логируют или пробрасывают дальше тому, кто умеет обработать. Молча игнорировать — нельзя.


Смежные темы: промисы и async/await — асинхронные ошибки; классы — свои типы ошибок; обработка ошибок в Node.js. Полный список — в уроках JavaScript.

Частые вопросы

Как работает try/catch?
Код в блоке try выполняется, и если внутри возникает ошибка, управление сразу переходит в блок catch с объектом ошибки. Без ошибки catch пропускается. Блок finally, если есть, выполняется всегда.
Что бросать через throw — строку или объект?
Всегда объект Error или его наследника: throw new Error(‘текст’). У Error есть message, name и stack — стек вызовов для отладки. Строка throw ‘текст’ лишает вас стека и ломает единообразие обработки.
Зачем нужен finally?
Блок finally выполняется всегда — и при успехе, и при ошибке. В него кладут освобождение ресурсов: закрыть файл, соединение, убрать индикатор загрузки. Он сработает, даже если в try был return.
Как ловить ошибки в async/await?
Обычным try/catch вокруг await: try { await fetch() } catch (e) {}. Асинхронная ошибка становится отклонённым промисом, а await превращает его в исключение, которое ловит catch.
Почему нельзя глотать ошибку пустым catch?
Пустой catch {} прячет проблему: программа продолжает работать в сломанном состоянии, а вы не узнаете о баге. Ошибку нужно либо обработать осмысленно, либо залогировать, либо пробросить дальше.