Любой разработчик регулярно ловит ошибки. Сообщение в консоли вида TypeError: Cannot read property 'name' of undefined выглядит однотипно, но за каждым таким сообщением стоит конкретный тип объекта-ошибки, и от типа зависит, где именно искать причину.

В JavaScript встроенных типов ошибок не так много — меньше десятка, и почти все они унаследованы от общего базового класса Error. Разберёмся, кто из них когда возникает, чем отличаются compile-time и runtime ошибки, как их корректно ловить и когда имеет смысл объявлять собственный класс ошибки.

Базовый класс Error

Все встроенные ошибки — SyntaxError, TypeError, RangeError и остальные — это классы, наследующиеся от Error. У каждого экземпляра есть три главных поля:

  • name — имя класса ("TypeError", "RangeError" и т. д.);
  • message — человеческое описание;
  • stack — стек вызовов на момент возникновения (нестандартизированное, но есть у всех движков).

Зная иерархию, можно одним catch ловить разные ошибки и принимать решения через instanceof — об этом в разделе про обработку.

Ошибки на этапе разбора кода

SyntaxError

Возникает, когда код не парсится: пропущена скобка, не закрыта строка, перепутан оператор. Главная особенность — до выполнения дело не доходит: движок ругается на этапе разбора, поэтому никакой try/catch такую ошибку не ловит (если только это не eval или JSON.parse над невалидной строкой).

const handler = () => console.log("hi") };
// SyntaxError: Unexpected token '}'

let my-var = 42;
// SyntaxError: Unexpected token '-'

Лечится в редакторе: линтер и подсветка синтаксиса вылавливают подобное ещё до запуска. Если перешли на TypeScript — компилятор не даст собрать проект, см. отдельную статью про TypeScript.

Ошибки во время выполнения

Эти ошибки бросаются уже после того, как файл успешно распарсился, и их можно ловить через try/catch.

ReferenceError

Возникает при обращении к переменной, которой не существует или которая недоступна в текущем скоупе.

console.log(score);
// ReferenceError: score is not defined

function inner() {
  const local = 10;
}
inner();
console.log(local);
// ReferenceError: local is not defined

Ещё один частый случай — обращение к переменной до её объявления в зоне temporal dead zone для let/const:

console.log(price);
let price = 100;
// ReferenceError: Cannot access 'price' before initialization

TypeError

Бросается, когда операция применяется к значению неподходящего типа: вызов несуществующего метода, чтение свойства у undefined или null, попытка изменить замороженный объект.

const count = 5;
count.toUpperCase();
// TypeError: count.toUpperCase is not a function

const user = { name: "Anna" };
console.log(user.address.city);
// TypeError: Cannot read properties of undefined (reading 'city')

Самый часто встречающийся тип ошибки в реальной работе. Современные практики — опциональная цепочка user?.address?.city и оператор нулевого слияния ?? — во многом созданы именно для борьбы с TypeError.

RangeError

Бросается, когда числовой аргумент или индекс попал за допустимые границы операции. Стоит отдельно проговорить: обращение к несуществующему индексу массива — это не RangeError, оно тихо возвращает undefined. RangeError бросается только тогда, когда метод заранее объявил допустимый диапазон.

new Array(-1);
// RangeError: Invalid array length

(123).toFixed(101);
// RangeError: toFixed() digits argument must be between 0 and 100

new Date(NaN).toISOString();
// RangeError: Invalid time value

function recurse() { recurse(); }
recurse();
// RangeError: Maximum call stack size exceeded

Последний пример — переполнение стека — одна из самых громких разновидностей RangeError. В Firefox для этого случая исторически использовался отдельный InternalError, но это уже Firefox-only расширение.

URIError

Появляется, когда функции работы с URI получают строку, которую невозможно корректно обработать. Самый распространённый случай — кривой %-эскейп в decodeURIComponent.

decodeURIComponent("%E0%A4%A");
// URIError: URI malformed

decodeURIComponent("%");
// URIError: URI malformed

На практике встречается нечасто — обычно когда серверная часть отдала строку с экранированными символами в неполном виде или клиент сам что-то слепил конкатенацией.

AggregateError

Появился сравнительно недавно — для случая, когда нужно сообщить сразу о нескольких ошибках одной. Главный пользователь — метод Promise.any: если все переданные промисы отклонились, он бросает AggregateError со списком причин в поле errors.

const fast = Promise.reject(new Error("source A is down"));
const slow = Promise.reject(new Error("source B is down"));

try {
  await Promise.any([fast, slow]);
} catch (err) {
  console.log(err.name);    // AggregateError
  console.log(err.message); // All promises were rejected
  console.log(err.errors);  // [Error: 'source A is down', Error: 'source B is down']
}

Конструктор можно вызвать и руками, если своя операция возвращает несколько одновременных проблем:

throw new AggregateError(
  [new TypeError("not a number"), new RangeError("out of bounds")],
  "Validation failed"
);
Поддержка браузерами
chrome
Chrome
85
firefox
Firefox
79
edge
Edge
85
safari
Safari
14
opera
Opera
71

Исторические и редкие

В чужом коде иногда мелькают ещё два класса — EvalError и InternalError. Оба сегодня в продакшене встречать почти не приходится, но знать о них полезно.

  • EvalError — раньше бросался при некорректном использовании eval(). Со времён ECMAScript 2015 (ES6) современные движки его уже не кидают; класс остался только ради совместимости и иногда используется в самописных утилитах в стиле throw new EvalError(...).
  • InternalError — нестандартное расширение Firefox для случаев типа «слишком глубокая рекурсия». В Chrome, Safari, Edge его нет — там вместо него летит RangeError: Maximum call stack size exceeded.

В новом коде эти классы не используют. Если встретили в логах — источник либо очень старый, либо Firefox-специфичный.

Как обрабатывать

Стандартная конструкция для работы с runtime-ошибками — try/catch/finally:

try {
  const data = await fetchData();
  process(data);
} catch (err) {
  console.error("Не удалось обработать данные:", err);
} finally {
  hideLoadingSpinner();
}

Блок finally выполняется всегда — и при успехе, и при выброшенной ошибке. Удобен для уборки: закрыть соединение, погасить лоадер, освободить ресурс.

В одном catch часто прилетают ошибки разных типов. Чтобы реагировать по-разному, помогает instanceof:

try {
  validateAndSave(form);
} catch (err) {
  if (err instanceof TypeError) {
    showInlineError(err.message);
  } else if (err instanceof RangeError) {
    showInlineError("Значение выходит за допустимые границы");
  } else {
    reportToSentry(err);
    throw err; // пробросить дальше всё, что не знаем
  }
}

Принцип «пробросить дальше всё, что не умею обработать» — ключевой. Глотать незнакомые ошибки catch (err) { /* ничего */ } — верный способ скрыть баг и потом долго его ловить.

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

Для бизнес-логики удобно объявлять собственные классы. Это позволяет в catch различать «не пришёл ответ от сервера», «форма не прошла валидацию» и «превышена квота» одним и тем же instanceof.

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

class ApiError extends Error {
  constructor(message, status) {
    super(message);
    this.name = "ApiError";
    this.status = status;
  }
}

try {
  await submit(form);
} catch (err) {
  if (err instanceof ValidationError) {
    highlightField(err.field);
  } else if (err instanceof ApiError && err.status === 401) {
    redirectToLogin();
  } else {
    throw err;
  }
}

Главное правило — всегда проставлять this.name в конструкторе. Без него имя класса в логах и в err.toString() останется родительским Error, и читать стек становится неудобно.

Итог

JavaScript описывает не так уж много типов ошибок, и большинство из них появляются в коде каждый день: SyntaxError ловит редактор и линтер, ReferenceError и TypeError прилетают в консоль чаще всего, RangeError и URIError — реже и в более узких сценариях, AggregateError сопровождает Promise.any. Конструкция try/catch/finally и проверка через instanceof покрывают большинство задач, а собственные классы-наследники Error делают код предсказуемее, когда у бизнес-логики есть свои «ожидаемые» виды сбоев. Если эта тема в JS у вас в целом ещё в стадии освоения — начните с обзорной статьи про JavaScript.