Обработка ошибок в async коде

В асинхронном коде ошибки перехватываются через .catch у Promise-цепочек или блок try/catch внутри async-функций; необработанные отклонения Promise приводят к UnhandledPromiseRejection.

Зачем нужно

Синхронный try/catch не ловит ошибки в асинхронных операциях — это частый источник «немых» сбоев. Понимание того, как ошибки распространяются через Promise и async/await, критически важно для написания надёжных приложений с корректной обратной связью пользователю и логированием.

Где используется

  • Запросы к API (fetch, axios)
  • Работа с базами данных, файловой системой (Node.js)
  • Цепочки Promise.all, Promise.allSettled
  • Middleware в Express/Fastify

.catch в цепочке Promise

fetch('/api/data')
  .then(res => {
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  })
  .then(data => console.log(data))
  .catch(err => {
    // Ловит ошибки из ВСЕХ .then выше
    console.error('Ошибка запроса:', err.message);
  })
  .finally(() => {
    // Выполняется всегда: успех или ошибка
    console.log('Запрос завершён');
  });

try/catch с async/await

async function loadUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`Пользователь не найден: ${res.status}`);
    const user = await res.json();
    return user;
  } catch (err) {
    // Ловит и сетевые ошибки, и throw выше
    console.error('Ошибка загрузки:', err.message);
    return null;
  } finally {
    console.log('loadUser завершён');
  }
}

Паттерн: go-style обёртка

// Возвращает [error, data] вместо throw
async function to(promise) {
  try {
    const data = await promise;
    return [null, data];
  } catch (err) {
    return [err, null];
  }
}

async function main() {
  const [err, user] = await to(fetch('/api/user').then(r => r.json()));
  if (err) {
    console.error('Ошибка:', err.message);
    return;
  }
  console.log('Пользователь:', user.name);
}

Promise.all и обработка ошибок

// Promise.all — прерывается при первой ошибке
try {
  const [users, posts] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json())
  ]);
} catch (err) {
  // Ловим ошибку любого из запросов
  console.error(err);
}

// Promise.allSettled — дожидается всех, даже при ошибках
const results = await Promise.allSettled([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/posts').then(r => r.json())
]);

results.forEach(result => {
  if (result.status === 'fulfilled') {
    console.log('Данные:', result.value);
  } else {
    console.error('Ошибка:', result.reason.message);
  }
});

Глобальный обработчик необработанных отклонений

// В браузере
window.addEventListener('unhandledrejection', (event) => {
  console.error('Необработанный rejection:', event.reason);
  event.preventDefault(); // подавить вывод в консоль браузера
});

// В Node.js
process.on('unhandledRejection', (reason, promise) => {
  console.error('Необработанный rejection:', reason);
});

Цепочка ошибок (Error cause)

async function fetchUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    return await res.json();
  } catch (err) {
    throw new Error(`Не удалось загрузить пользователя ${id}`, { cause: err });
  }
}

try {
  await fetchUser(42);
} catch (err) {
  console.error(err.message);       // Не удалось загрузить пользователя 42
  console.error(err.cause.message); // Исходная причина (fetch error)
}

Частые ошибки

1. Забытый await — ошибка не поймается

async function bad() {
  try {
    fetchData; // без await — try/catch не поймает!
  } catch (err) {
    console.error(err); // никогда не выполнится
  }
}

async function good() {
  try {
    await fetchData; // правильно
  } catch (err) {
    console.error(err);
  }
}

2. Не возвращён Promise из .then

// Ошибка в вложенном .then не поймается внешним .catch
fetch('/api')
  .then(res => {
    res.json().then(data => { // вложенный промис — антипаттерн
      throw new Error('Ошибка'); // не поймается!
    });
  })
  .catch(err => console.error(err)); // не сработает

// Правильно: возвращай промис из .then
fetch('/api')
  .then(res => res.json())    // возвращаем Promise
  .then(data => { throw new Error('Ошибка'); })
  .catch(err => console.error(err)); // сработает

3. Повторный throw без контекста

// Плохо: теряем исходный стек
catch (err) { throw new Error('Что-то пошло не так'); }

// Хорошо: сохраняем причину
catch (err) { throw new Error('Что-то пошло не так', { cause: err }); }

Связанные темы

Ресурсы