> Как отлавливать ошибки промисов в JavaScript (JavaScript)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: циан, Яндекс

Стек: JavaScript

> Пример ответа

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

Основной способ - метод .catch() на промисах и блок try/catch в async/await. Для глобального перехвата необработанных rejections используются события unhandledrejection в браузере и process.on('unhandledRejection') в Node.js. Важно помнить: ошибка в цепочке промисов "падает" до ближайшего .catch(), а в async-функции - до ближайшего try/catch. Если rejection не обработан - это утечка и потенциальный крах приложения.

Подробное объяснение

Промисы имеют три состояния: pending, fulfilled, rejected. Ошибка возникает при вызове reject() или при throw внутри executor/async-функции. Существует два синтаксических подхода:

  1. Цепочки .then().catch() - .catch() перехватывает ошибки из предыдущих .then() и самого промиса. Важно: .catch() возвращает новый промис, поэтому цепочку можно продолжать.

  2. async/await - синтаксический сахар над промисами. await приостанавливает выполнение функции, а при rejection выбрасывает исключение, которое ловится try/catch. Если исключение не поймано внутри async-функции - она возвращает rejected промис.

Глобальный перехват нужен для логирования и предотвращения "тихих" ошибок. В браузере: window.addEventListener('unhandledrejection', handler). В Node.js: process.on('unhandledRejection', handler). Событие rejectionhandled срабатывает, когда ошибка была обработана позже (после события unhandledrejection).

Также есть Promise.allSettled() - он не бросает ошибку, а возвращает массив результатов с полями status, value/reason. Это полезно, когда нужно обработать все промисы независимо от их исхода.

На практике

Для senior-уровня важно не просто знать синтаксис, а понимать:

  • Порядок обработки: .catch() ловит ошибки только из предыдущих звеньев цепочки, не из последующих.
  • Возврат из .catch(): если вернуть значение - цепочка продолжится как успешная; если бросить снова - ошибка уйдёт дальше.
  • Асинхронные ошибки в try/catch: try/catch не ловит ошибки из колбэков, например, из setTimeout или addEventListener.
  • Ошибки в конструкторе промиса: если в executor брошено исключение - промис автоматически становится rejected.
  • Необработанные rejection в тестах: в Jest/Playwright они могут приводить к падению тестов, поэтому нужно явно обрабатывать или использовать unhandledRejection для диагностики.

На практике часто используют утилиту to() (из golang-паттерна), которая возвращает [data, error] вместо try/catch - это упрощает чтение кода.

Пример кода

JAVASCRIPT
// Базовый catch
fetch('/api/user')
.then(res => res.json())
.catch(err => console.error('Ошибка запроса:', err));
// async/await
async function loadUser() {
try {
const res = await fetch('/api/user');
const data = await res.json();
return data;
} catch (err) {
console.error('Ошибка:', err);
throw err; // пробрасываем дальше, если нужно
}
}
// Глобальный перехват в браузере
window.addEventListener('unhandledrejection', (event) => {
console.warn('Необработанный rejection:', event.reason);
event.preventDefault(); // предотвращает лог в консоль
});
// Promise.allSettled
const results = await Promise.allSettled([
fetch('/api/a'),
fetch('/api/b'),
]);
results.forEach(r => {
if (r.status === 'fulfilled') {
console.log('Успех:', r.value);
} else {
console.log('Ошибка:', r.reason);
}
});
// Утилита to()
async function to(promise) {
try {
const data = await promise;
return [null, data];
} catch (err) {
return [err, null];
}
}
const [err, user] = await to(fetch('/api/user'));
if (err) {
// обработка ошибки
}

Как отвечать на собеседовании

Начни с базового .catch() и try/catch, затем переходи к глобальным обработчикам. Упомяни разницу между браузером и Node.js. Покажи понимание цепочек: что .catch() возвращает новый промис, что ошибка "проваливается" до ближайшего обработчика. Добавь про allSettled как альтернативу, когда не нужно прерывать выполнение. Если спросят про "тихие" ошибки - расскажи про unhandledrejection и почему это плохо для продакшена. Хорошо упомянуть паттерн to() как практический приём.

Что проверяет интервьюер

  • Понимание асинхронной модели JavaScript: event loop, microtasks, порядок выполнения.
  • Умение работать с цепочками промисов и их особенностями (возврат значений, проброс ошибок).
  • Знание глобальных обработчиков и их назначения.
  • Способность выбирать правильный инструмент под задачу: .catch(), try/catch, allSettled, finally.
  • Понимание последствий необработанных rejection: утечки памяти, падение процесса в Node.js.

Типичные ошибки

  • Забывают, что try/catch не ловит ошибки внутри setTimeout или колбэков событий.
  • Путают порядок .then() и .catch(): .catch() после .then() ловит ошибки только из этого .then(), не из предыдущих.
  • Не возвращают промис из .catch() и теряют цепочку.
  • Игнорируют unhandledRejection в Node.js - это приводит к падению процесса (в новых версиях Node).
  • Используют Promise.all() без обработки ошибок, когда нужно получить частичные результаты - лучше allSettled.
  • Пробрасывают ошибку из catch без throw - и потом удивляются, что внешний код не видит ошибку.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью