Back

Написание более чистых асинхронных цепочек с Promise.try

Написание более чистых асинхронных цепочек с Promise.try

Если вы когда-либо писали цепочку промисов, которая начинается с функции, которая может быть синхронной или может быть асинхронной, вы, вероятно, сталкивались с неудобной проблемой: куда поместить .catch()?

Синхронные ошибки, выброшенные до возврата промиса, не будут перехвачены .catch(), если вы ещё не находитесь внутри контекста промиса. Promise.try() решает эту проблему элегантно, предоставляя единую, последовательную точку входа для любой цепочки промисов — независимо от того, является ли вызываемая функция синхронной, асинхронной или чем-то промежуточным.

Ключевые выводы

  • Синхронные исключения внутри функций, которые иногда возвращают промисы, могут полностью избежать .catch(), что приводит к необработанным исключениям.
  • Promise.try() выполняет функцию немедленно и оборачивает как синхронные возвраты, так и синхронные исключения в правильный промис, предоставляя унифицированный путь обработки ошибок.
  • В отличие от Promise.resolve(fn()), он перехватывает синхронные ошибки. В отличие от Promise.resolve().then(fn), он выполняется немедленно, не откладывая выполнение в микрозадачу.
  • Он лучше всего подходит для цепочек на основе .then() со смешанными синхронными/асинхронными точками входа — не как замена async/await.

Проблема: синхронные ошибки, которые избегают вашу цепочку .catch()

Рассмотрим загрузчик данных, который читает из кеша синхронно или получает данные из API асинхронно в зависимости от условий:

function loadData(key) {
  const cached = getFromCache(key) // may throw synchronously
  if (cached) return cached
  return fetch(`/api/data/${key}`).then(res => res.json())
}

loadData('user-1')
  .then(data => render(data))
  .catch(err => handleError(err)) // ⚠️ Won't catch sync throws from loadData

Если getFromCache выбрасывает исключение синхронно, эта ошибка никогда не будет перехвачена .catch(). Исключение происходит до того, как существует какой-либо промис, поэтому оно полностью избегает цепочку и становится необработанным исключением.

Здесь стоит отметить второй тонкий момент: когда loadData возвращает кешированное значение напрямую (не-thenable), вызов .then() на нём также завершится ошибкой, потому что обычные значения не имеют метода .then(). Эта функция по своей природе хрупкая — она возвращает промис в одной ветке и обычное значение в другой. Promise.try() решает обе проблемы, всегда создавая промис.

Как Promise.try() исправляет это

Promise.try(fn) выполняет предоставленную функцию немедленно и оборачивает результат в промис. Если функция возвращает обычное значение, промис разрешается с этим значением. Если она возвращает промис, он принимает этот промис. Если она выбрасывает исключение синхронно, оно преобразуется в отклонение.

Это даёт вам единую точку входа для обработки как синхронных, так и асинхронных ошибок через один и тот же .catch():

Promise.try(() => loadData('user-1'))
  .then(data => render(data))
  .catch(err => handleError(err)) // ✅ Catches both sync throws and async rejections

Никаких особых случаев. Никакого оборачивания в try/catch перед началом цепочки. Всё проходит через .catch(), как и ожидается.

Чем он отличается от Promise.resolve().then(fn) и Promise.resolve(fn())

Эти два паттерна обычно используются как обходные пути, но они ведут себя по-разному в важных аспектах.

Promise.resolve(fn()) вызывает fn() немедленно, вне контекста промиса. Синхронное исключение здесь является неперехваченным исключением, а не отклонением.

Promise.resolve().then(fn) откладывает выполнение fn в микрозадачу. Это означает, что fn не выполняется немедленно — что может вызвать тонкие проблемы с синхронизацией и делает поведение менее предсказуемым, когда вам нужно немедленное выполнение.

Promise.try(fn) выполняет fn немедленно и перехватывает любое синхронное исключение как отклонение. Это наиболее предсказуемый из трёх вариантов для начала цепочек промисов.

ПаттернВыполняет fn немедленноПерехватывает синхронные исключения
Promise.resolve(fn())
Promise.resolve().then(fn)
Promise.try(fn)

Практические случаи использования во фронтенде

Promise.try() естественно вписывается в асинхронные паттерны JavaScript, где поведение зависит от условий выполнения:

Утилитарные функции, которые могут возвращать кешированные данные синхронно или получать их асинхронно:

Promise.try(() => getUserFromCacheOrAPI(userId))
  .then(updateUI)
  .catch(showErrorBanner)

Условные асинхронные рабочие процессы, где шаг валидации может выбросить исключение до начала любой асинхронной работы:

Promise.try(() => {
  validateInput(formData) // throws if invalid
  return submitForm(formData) // returns a promise
})
  .then(handleSuccess)
  .catch(handleError)

Поддержка браузерами и совместимость

Promise.try() был включён в ECMAScript 2025 (ES2025) и поддерживается в Chrome 128+, Firefox 134+, Safari 18.2+ и Node.js 22.7.0+. Вы можете проверить текущую поддержку браузерами на Can I Use, который отслеживает статус реализации в основных браузерах и средах выполнения.

Для более старых сред вы можете использовать полифилл с простой обёрткой:

Promise.try = Promise.try || function(fn) {
  return new Promise(resolve => resolve(fn()))
}

Это работает, потому что исполнитель new Promise выполняется синхронно, поэтому fn() вызывается немедленно. Если fn() выбрасывает исключение, конструктор Promise перехватывает его и превращает в отклонение. Если fn() возвращает thenable, resolve принимает его.

Заключение

Promise.try() не является заменой async/await. Это небольшой, целенаправленный инструмент для одной конкретной ситуации: начала цепочки промисов, когда точка входа может выбросить исключение синхронно или вернуть смесь значений и промисов.

Если вы уже находитесь внутри async функции, блок try/catch обрабатывает оба случая естественным образом. Но когда вы работаете с цепочками .then() — особенно с утилитарными функциями или загрузчиками данных с условной логикой — Promise.try() делает вашу обработку ошибок последовательной, а цепочки чистыми.

Часто задаваемые вопросы

Да. Если функция, которую вы передаёте в Promise.try(), является async, она возвращает промис, и Promise.try() принимает этот промис. Это работает так же, как передача любой функции, возвращающей промис. Основное преимущество Promise.try() заключается в функциях, которые могут вообще не возвращать промис или могут выбросить исключение до его возврата.

Нет. Внутри async функции стандартный блок try/catch уже перехватывает как синхронные исключения, так и ожидаемые отклонения. Promise.try() предназначен для цепочек в стиле .then(), где вам нужна безопасная точка входа. Если вы уже используете async/await, вам, вероятно, не нужен Promise.try().

Распространённый полифилл, использующий new Promise(resolve => resolve(fn())), функционально эквивалентен нативной реализации. Он выполняет fn немедленно, перехватывает синхронные исключения через конструктор Promise и принимает thenable через resolve. Он безопасен для использования в продакшене в средах, где отсутствует нативная поддержка.

Promise.resolve(fn()) вызывает fn вне контекста промиса, поэтому синхронные исключения становятся неперехваченными исключениями. Promise.resolve().then(fn) перехватывает исключения, но откладывает выполнение в микрозадачу, что означает, что fn не выполняется немедленно. Promise.try(fn) — единственный паттерн, который и выполняет fn немедленно, и перехватывает синхронные ошибки как отклонения.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay