TGViewer
Настоящий JavaScript Настоящий JavaScript @true_js · 5.89K subscribers
Post #3684 426
⁣Неявные баги в Promise.withResolvers() при передаче управления между модулями в ESM-циклах и динамических импортах

Этот баг пропускают и TypeScript, и линтер — вы узнаете о нём только на проде. Promise.withResolvers() (ES2024) в ESM-циклах создаёт состояние гонки, когда resolve/reject экспортируются из модуля, участвующего в циклических зависимостях.

Как работает гонка

В ESM модули выполняются синхронно при импорте. Если модуль A экспортирует resolve, а модуль B вызывает его на этапе инициализации, resolve может быть undefined или ссылка на него может быть перезаписана после вызова. Пример:
// module-a.ts
const { promise, resolve } = Promise.withResolvers<void>();
export { promise, resolve };

// module-b.ts
import { resolve } from './module-a';
resolve(); // Вызов до завершения module-a


Проблема с динамическим импортом

Динамический импорт добавляет ещё один слой неопределённости. Промис может быть уже разрешён до момента, когда импорт завершится:
// module-c.ts
const { promise, resolve } = Promise.withResolvers();
export default promise;
export { resolve };

// вызов
const module = await import('./module-c');
module.resolve('value'); // Промис уже был разрешён — вызов игнорируется


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

* Не экспортируйте resolve/reject из модуля, который участвует в циклических зависимостях — это гарантирует частичное выполнение модуля и некорректные ссылки.
* При динамическом импорте устанавливайте тайм-аут для промиса с Promise.race(), чтобы не зависнуть навсегда.
* Для сложной синхронизации между модулями используйте EventEmitter или очереди — они явно управляют порядком выполнения.

Бонусная ловушка

resolve/reject — обычные функции, их можно вызывать сколько угодно раз. В ESM-циклах повторный вызов просто игнорируется, но вы не заметите этого на тестах. На проде — вечно висящий промис или пропущенный reject.

Чек-лист для production:
- Promise.withResolvers() — только внутри модуля, никогда не экспортируя resolve или reject наружу.
- Никаких асинхронных вызовов на верхнем уровне модуля, если экспортируете resolve.
- Если архитектура требует внешнего управления — используйте AbortController для отмены.

Вывод: Promise.withResolvers() безопасен только в изолированном контексте; экспорт resolve/reject в ESM-циклах превращает код в недетерминированную гонку, которую невозможно отловить статическим анализом.
More from @true_js
  1. Oct 2, 2026😅 Айтишник отправил в одну компанию три одинаковых резюме и только одно дошло до финала О…
  2. Oct 2, 2026Post #3961
  3. Oct 2, 2026🤣 Правильно расставленные приоритеты в моей жизни би лайк: 💥 xCode Journal
  4. Oct 1, 2026🤯 Люди взбунтовались против «пыточной для ИИ» На GitHub заметили открытый проект AI Tortu…
  5. Oct 1, 2026День в Заонежье начинается ещё в дороге. Мы встретим вас в аэропорту или на вокзале. Дальш…
  6. Oct 1, 2026Post #3946
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →