Как правильно обрабатывать ошибки в JavaScript?

MiddleJavaScript #javascript #errors #debugging

Вопрос

Как работает try/catch/finally, какие встроенные подклассы Error существуют, как создавать собственные классы ошибок и как ловить ошибки в асинхронном коде и отклонённых промисах?

Короткий ответ

try/catch/finally перехватывает синхронные исключения: catch выполняется при ошибке, finally — всегда, независимо от результата. Встроенные подклассы Error (TypeError, RangeError, ReferenceError и др.) описывают конкретные категории ошибок. Свои ошибки создают через class MyError extends Error. Асинхронные ошибки в async/await ловятся тем же try/catch, а непойманные отклонения промисов — через .catch() или глобальный обработчик unhandledrejection.

Подробный ответ

try/catch/finally. Блок try выполняет код, catch(error) перехватывает выброшенное исключение, finally выполняется в любом случае — даже если в try или catch есть return или новое исключение. finally полезен для гарантированной очистки ресурсов (закрытие соединений, снятие лоадеров). Важно: try/catch перехватывает только синхронные ошибки в своём блоке — ошибка, брошенная внутри setTimeout или в необработанном промисе внутри try, не будет поймана.

Объект Error и его подклассы. Каждая ошибка имеет message, name и stack (трассировку вызовов, не входит в спецификацию, но поддерживается везде). Встроенные подклассы:

  • TypeError — операция над значением неподходящего типа (null.foo, вызов не-функции).
  • RangeError — числовое значение вне допустимого диапазона (new Array(-1), toFixed(101)).
  • ReferenceError — обращение к необъявленной переменной.
  • SyntaxError — некорректный синтаксис (обычно при eval/JSON.parse).
  • URIError — ошибка в функциях encodeURI/decodeURI.
  • EvalError — устарел, практически не встречается.

Кастомные ошибки. Наследуются от Error через class, что даёт доступ к instanceof для различения типов ошибок в коде, обрабатывающем разные сценарии по-разному (валидация, сеть, авторизация и т.д.). Важно вызывать super(message) и явно задавать this.name, иначе name останется 'Error'.

Ошибки в асинхронном коде:

  • В async function любое исключение (в том числе throw и отклонённый await) превращается в отклонённый промис и ловится обычным try/catch вокруг await.
  • Промис без await требует .catch() в цепочке — иначе получаем unhandled promise rejection.
  • Promise.all падает целиком при первом отклонении — если нужно собрать результаты и ошибки отдельно, используют Promise.allSettled.
  • Глобально отловить пропущенные отклонения промисов можно через событие window.addEventListener('unhandledrejection', ...) в браузере или process.on('unhandledRejection', ...) в Node.js — полезно для логирования, но не заменяет локальную обработку.

Пример

// Базовый try/catch/finally
function parseConfig(json) {
  try {
    return JSON.parse(json)
  } catch (error) {
    console.error('Некорректный JSON:', error.message) // SyntaxError
    return null
  } finally {
    console.log('Парсинг завершён') // выполнится в любом случае
  }
}

// Встроенные подклассы Error
try {
  null.foo() // TypeError: Cannot read properties of null
} catch (e) {
  console.log(e instanceof TypeError, e.name) // true "TypeError"
}

// Кастомный класс ошибки
class ValidationError extends Error {
  constructor(message, field) {
    super(message)
    this.name = 'ValidationError'
    this.field = field
  }
}

function validateAge(age) {
  if (age < 0) {
    throw new ValidationError('Возраст не может быть отрицательным', 'age')
  }
  return age
}

try {
  validateAge(-5)
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Ошибка поля "${error.field}": ${error.message}`)
  } else {
    throw error // не наша ошибка — пробрасываем дальше
  }
}

// Асинхронные ошибки
async function fetchUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`)
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`)
    }
    return await response.json()
  } catch (error) {
    console.error('Не удалось загрузить пользователя:', error.message)
    return null
  }
}

// Promise.allSettled — не падает при первой ошибке
const results = await Promise.allSettled([
  fetchUser(1),
  fetchUser(-1), // допустим, отклонится
])
results.forEach((r) =>
  r.status === 'fulfilled' ? console.log(r.value) : console.log('Ошибка:', r.reason)
)

Дополнительные вопросы

  • Чем Promise.all отличается от Promise.allSettled и Promise.any в обработке ошибок?
  • Как свойство cause в new Error(message, { cause }) помогает при цепочке ошибок?
  • Почему finally с return внутри может "перекрыть" return/throw из try/catch?