← JavaScript/Промисы: обработка ошибок#124 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Промисы: обработка ошибок

Мобильное приложение периодически теряет соединение. Запрос к API падает с ошибкой — и вместо красивого сообщения пользователь видит белый экран. Причина: промис отклонён, но .catch() нигде не добавлен. В Node.js это ещё хуже — необработанный rejection убивает весь процесс.

На основе предыдущих уроков

  • «Промисы» — resolve, reject, цепочка .then()
  • «try/catch» — синхронная обработка ошибок; .catch() — её асинхронный аналог
  • «Пользовательские ошибки» — Error, TypeError, их свойство message
  • «Promise.all» — Promise.allSettled для обработки нескольких промисов
  • .catch() — перехват ошибок

    fetch('/api/user/1')
      .then(res => res.json())
      .then(user => renderProfile(user))
      .catch(err => {
        // Ловит ошибки из ЛЮБОГО места цепочки выше
        console.error('Что-то пошло не так:', err.message)
        showErrorBanner('Не удалось загрузить профиль')
      })

    Ошибка в .then() → ближайший .catch()

    Если исключение брошено внутри .then(), оно автоматически попадает в следующий .catch():

    Promise.resolve({ status: 500 })
      .then(res => {
        if (res.status !== 200) throw new Error('Сервер вернул ошибку')
        return res.json()
      })
      .then(data => console.log(data))  // пропускается
      .catch(err => console.error(err.message))  // 'Сервер вернул ошибку'

    .catch() может восстановить цепочку

    getSettings()
      .catch(err => {
        console.warn('Не удалось загрузить настройки, используем дефолтные')
        return { theme: 'light', language: 'ru' }  // восстанавливаем значение
      })
      .then(settings => applySettings(settings))   // выполнится в любом случае

    Unhandled Promise Rejection

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

    // Плохо: нет обработки ошибок
    fetch('/api/data').then(r => r.json())  // UnhandledPromiseRejection!
    
    // Хорошо:
    fetch('/api/data')
      .then(r => r.json())
      .catch(err => handleError(err))  // всегда добавляй .catch()

    Глобальный обработчик: unhandledrejection

    window.addEventListener('unhandledrejection', event => {
      console.error('Необработанная ошибка промиса:', event.reason)
      event.preventDefault()  // предотвращает вывод в консоль
      // отправить в систему мониторинга (Sentry, Datadog...)
      logToMonitoring({ error: event.reason, url: location.href })
    })

    В Node.js аналог: process.on('unhandledRejection', handler).

    Retry-логика для ненадёжных запросов

    Сетевые запросы могут падать по временным причинам. Простой паттерн повтора:

    function retry(fn, times, delay = 0) {
      return fn().catch(err => {
        if (times <= 0) return Promise.reject(err)
        return new Promise(resolve => setTimeout(resolve, delay))
          .then(() => retry(fn, times - 1, delay * 2))  // экспоненциальный backoff
      })
    }
    
    // Повторить запрос до 3 раз с задержкой 100→200→400мс
    retry(
      () => fetch('/api/orders').then(r => r.json()),
      3,
      100
    )
      .then(orders => renderOrders(orders))
      .catch(err => showError('Не удалось загрузить заказы после 3 попыток'))

    Экспоненциальный backoff

    Каждая следующая попытка ждёт в 2 раза дольше, чтобы не перегружать сервер:

  • Попытка 1: сразу
  • Попытка 2: через 100мс
  • Попытка 3: через 200мс
  • Попытка 4: через 400мс
  • Promise.allSettled — когда важен каждый результат

    Promise.allSettled([
      fetch('/api/user'),
      fetch('/api/orders'),
      fetch('/api/recommendations'),
    ])
    .then(results => {
      results.forEach((result, i) => {
        if (result.status === 'fulfilled') {
          console.log(`Запрос ${i} успешен`)
        } else {
          console.warn(`Запрос ${i} упал: ${result.reason.message}`)
        }
      })
    })

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

    1. Потерянный catch — ошибка внутри .then() не выбрасывается дальше автоматически:

    // Плохо: если renderProfile бросит ошибку — она нигде не поймается
    fetch('/api/user')
      .then(res => res.json())
      .then(user => renderProfile(user))
      // нет .catch() — UnhandledPromiseRejection!
    
    // Хорошо: всегда добавляй .catch() в конец цепочки
    fetch('/api/user')
      .then(res => res.json())
      .then(user => renderProfile(user))
      .catch(err => showError(err.message))

    2. Ошибка в .catch() — нужен ещё один .catch() или она потеряется:

    // Плохо: если handler в catch сам бросит ошибку — она потеряется
    fetch('/api/user')
      .catch(err => {
        throw new Error('обёртка: ' + err.message)  // эта ошибка никуда не дойдёт!
      })
    
    // Хорошо: добавь ещё один catch или логируй в finally
    fetch('/api/user')
      .catch(err => { throw new Error('обёртка: ' + err.message) })
      .catch(err => console.error('Финальная ошибка:', err.message))

    3. Async/await без try/catch — то же самое что промис без .catch():

    // Плохо: необработанная ошибка
    async function loadUser() {
      const res = await fetch('/api/user')  // может упасть
      return res.json()
    }
    
    // Хорошо: оборачивай await в try/catch
    async function loadUser() {
      try {
        const res = await fetch('/api/user')
        return res.json()
      } catch (err) {
        console.error('Не удалось загрузить пользователя:', err)
        return null
      }
    }

    В реальных проектах

  • Глобальный обработчик ошибок — window.addEventListener('unhandledrejection') для логирования в Sentry
  • Retry с backoff — повтор упавших запросов с увеличивающейся задержкой
  • Fallback данные — .catch(() => defaultValue) вместо показа ошибки пользователю
  • Circuit breaker — паттерн защиты: после N ошибок подряд перестаём делать запросы на время
  • Примеры

    Цепочка промисов с восстановлением ошибки и retry-логикой

    // Симуляция нестабильного API
    let attemptCount = 0
    function unstableApi(failTimes) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          attemptCount++
          if (attemptCount <= failTimes) {
            reject(new Error(`Попытка ${attemptCount}: сервер недоступен`))
          } else {
            resolve({ orders: [{ id: 1, total: 4500 }, { id: 2, total: 1200 }] })
          }
        }, 10)
      })
    }
    
    // Восстановление в .catch()
    Promise.resolve(null)
      .then(() => { throw new Error('Настройки не найдены') })
      .catch(err => {
        console.warn(err.message + ', используем дефолтные')
        return { theme: 'light', lang: 'ru' }  // восстановление
      })
      .then(settings => {
        console.log('Применяем настройки:', settings.theme)
      })
    
    // Retry с экспоненциальным backoff
    function retry(fn, times, delay = 0) {
      return fn().catch(err => {
        console.log(err.message)
        if (times <= 0) return Promise.reject(err)
        return new Promise(r => setTimeout(r, delay))
          .then(() => retry(fn, times - 1, delay === 0 ? 10 : delay * 2))
      })
    }
    
    // Сброс счётчика и тест
    attemptCount = 0
    retry(() => unstableApi(2), 3)
      .then(data => {
        console.log('Успех после нескольких попыток!')
        console.log('Заказов:', data.orders.length)
        const total = data.orders.reduce((sum, o) => sum + o.total, 0)
        console.log('Общая сумма:', total, '₽')
      })
      .catch(err => console.error('Все попытки исчерпаны:', err.message))

    Промисы: обработка ошибок

    Мобильное приложение периодически теряет соединение. Запрос к API падает с ошибкой — и вместо красивого сообщения пользователь видит белый экран. Причина: промис отклонён, но .catch() нигде не добавлен. В Node.js это ещё хуже — необработанный rejection убивает весь процесс.

    На основе предыдущих уроков

  • «Промисы» — resolve, reject, цепочка .then()
  • «try/catch» — синхронная обработка ошибок; .catch() — её асинхронный аналог
  • «Пользовательские ошибки» — Error, TypeError, их свойство message
  • «Promise.all» — Promise.allSettled для обработки нескольких промисов
  • .catch() — перехват ошибок

    fetch('/api/user/1')
      .then(res => res.json())
      .then(user => renderProfile(user))
      .catch(err => {
        // Ловит ошибки из ЛЮБОГО места цепочки выше
        console.error('Что-то пошло не так:', err.message)
        showErrorBanner('Не удалось загрузить профиль')
      })

    Ошибка в .then() → ближайший .catch()

    Если исключение брошено внутри .then(), оно автоматически попадает в следующий .catch():

    Promise.resolve({ status: 500 })
      .then(res => {
        if (res.status !== 200) throw new Error('Сервер вернул ошибку')
        return res.json()
      })
      .then(data => console.log(data))  // пропускается
      .catch(err => console.error(err.message))  // 'Сервер вернул ошибку'

    .catch() может восстановить цепочку

    getSettings()
      .catch(err => {
        console.warn('Не удалось загрузить настройки, используем дефолтные')
        return { theme: 'light', language: 'ru' }  // восстанавливаем значение
      })
      .then(settings => applySettings(settings))   // выполнится в любом случае

    Unhandled Promise Rejection

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

    // Плохо: нет обработки ошибок
    fetch('/api/data').then(r => r.json())  // UnhandledPromiseRejection!
    
    // Хорошо:
    fetch('/api/data')
      .then(r => r.json())
      .catch(err => handleError(err))  // всегда добавляй .catch()

    Глобальный обработчик: unhandledrejection

    window.addEventListener('unhandledrejection', event => {
      console.error('Необработанная ошибка промиса:', event.reason)
      event.preventDefault()  // предотвращает вывод в консоль
      // отправить в систему мониторинга (Sentry, Datadog...)
      logToMonitoring({ error: event.reason, url: location.href })
    })

    В Node.js аналог: process.on('unhandledRejection', handler).

    Retry-логика для ненадёжных запросов

    Сетевые запросы могут падать по временным причинам. Простой паттерн повтора:

    function retry(fn, times, delay = 0) {
      return fn().catch(err => {
        if (times <= 0) return Promise.reject(err)
        return new Promise(resolve => setTimeout(resolve, delay))
          .then(() => retry(fn, times - 1, delay * 2))  // экспоненциальный backoff
      })
    }
    
    // Повторить запрос до 3 раз с задержкой 100→200→400мс
    retry(
      () => fetch('/api/orders').then(r => r.json()),
      3,
      100
    )
      .then(orders => renderOrders(orders))
      .catch(err => showError('Не удалось загрузить заказы после 3 попыток'))

    Экспоненциальный backoff

    Каждая следующая попытка ждёт в 2 раза дольше, чтобы не перегружать сервер:

  • Попытка 1: сразу
  • Попытка 2: через 100мс
  • Попытка 3: через 200мс
  • Попытка 4: через 400мс
  • Promise.allSettled — когда важен каждый результат

    Promise.allSettled([
      fetch('/api/user'),
      fetch('/api/orders'),
      fetch('/api/recommendations'),
    ])
    .then(results => {
      results.forEach((result, i) => {
        if (result.status === 'fulfilled') {
          console.log(`Запрос ${i} успешен`)
        } else {
          console.warn(`Запрос ${i} упал: ${result.reason.message}`)
        }
      })
    })

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

    1. Потерянный catch — ошибка внутри .then() не выбрасывается дальше автоматически:

    // Плохо: если renderProfile бросит ошибку — она нигде не поймается
    fetch('/api/user')
      .then(res => res.json())
      .then(user => renderProfile(user))
      // нет .catch() — UnhandledPromiseRejection!
    
    // Хорошо: всегда добавляй .catch() в конец цепочки
    fetch('/api/user')
      .then(res => res.json())
      .then(user => renderProfile(user))
      .catch(err => showError(err.message))

    2. Ошибка в .catch() — нужен ещё один .catch() или она потеряется:

    // Плохо: если handler в catch сам бросит ошибку — она потеряется
    fetch('/api/user')
      .catch(err => {
        throw new Error('обёртка: ' + err.message)  // эта ошибка никуда не дойдёт!
      })
    
    // Хорошо: добавь ещё один catch или логируй в finally
    fetch('/api/user')
      .catch(err => { throw new Error('обёртка: ' + err.message) })
      .catch(err => console.error('Финальная ошибка:', err.message))

    3. Async/await без try/catch — то же самое что промис без .catch():

    // Плохо: необработанная ошибка
    async function loadUser() {
      const res = await fetch('/api/user')  // может упасть
      return res.json()
    }
    
    // Хорошо: оборачивай await в try/catch
    async function loadUser() {
      try {
        const res = await fetch('/api/user')
        return res.json()
      } catch (err) {
        console.error('Не удалось загрузить пользователя:', err)
        return null
      }
    }

    В реальных проектах

  • Глобальный обработчик ошибок — window.addEventListener('unhandledrejection') для логирования в Sentry
  • Retry с backoff — повтор упавших запросов с увеличивающейся задержкой
  • Fallback данные — .catch(() => defaultValue) вместо показа ошибки пользователю
  • Circuit breaker — паттерн защиты: после N ошибок подряд перестаём делать запросы на время
  • Примеры

    Цепочка промисов с восстановлением ошибки и retry-логикой

    // Симуляция нестабильного API
    let attemptCount = 0
    function unstableApi(failTimes) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          attemptCount++
          if (attemptCount <= failTimes) {
            reject(new Error(`Попытка ${attemptCount}: сервер недоступен`))
          } else {
            resolve({ orders: [{ id: 1, total: 4500 }, { id: 2, total: 1200 }] })
          }
        }, 10)
      })
    }
    
    // Восстановление в .catch()
    Promise.resolve(null)
      .then(() => { throw new Error('Настройки не найдены') })
      .catch(err => {
        console.warn(err.message + ', используем дефолтные')
        return { theme: 'light', lang: 'ru' }  // восстановление
      })
      .then(settings => {
        console.log('Применяем настройки:', settings.theme)
      })
    
    // Retry с экспоненциальным backoff
    function retry(fn, times, delay = 0) {
      return fn().catch(err => {
        console.log(err.message)
        if (times <= 0) return Promise.reject(err)
        return new Promise(r => setTimeout(r, delay))
          .then(() => retry(fn, times - 1, delay === 0 ? 10 : delay * 2))
      })
    }
    
    // Сброс счётчика и тест
    attemptCount = 0
    retry(() => unstableApi(2), 3)
      .then(data => {
        console.log('Успех после нескольких попыток!')
        console.log('Заказов:', data.orders.length)
        const total = data.orders.reduce((sum, o) => sum + o.total, 0)
        console.log('Общая сумма:', total, '₽')
      })
      .catch(err => console.error('Все попытки исчерпаны:', err.message))

    Задание

    Напиши функцию retry(fn, times), которая вызывает fn() и при отказе повторяет попытку до times раз. Если все попытки исчерпаны — возвращает промис с последней ошибкой. Если хотя бы одна попытка успешна — возвращает её результат.

    Подсказка

    Проверь times <= 0 для выброса ошибки. Для рекурсии: return retry(fn, times - 1). Это вернёт новый промис следующей попытки.

    Загружаем среду выполнения...
    Загружаем AI-помощника...