Promise.withResolvers() полезен на границе event-based API и async/await: WebSocket, Worker, IPC, SDK, API clients, Node.js-сервисы. Частая ошибка - считать его безопасной заменой любому deferred и хранить resolve/reject где попало.Что это даёт
const { promise, resolve, reject } =
Promise.withResolvers<T>();Это тот же внешний
resolve/reject, но без let, definite assignment и самодельного executor-шаблона. API стал чище, но lifecycle всё ещё ваша ответственность.Где применять
* ожидание одного события;
* bridge callback/event API в Promise;
* request/response поверх WebSocket, Worker или IPC;
* внутренняя очередь producer/consumer.
Не стоит передавать
resolve через слои, класть его в глобальное состояние или создавать promise без timeout/cancel path.Production-паттерн
type Message = { id: string; payload: unknown };
type Waiter = {
resolve: (m: Message) => void;
reject: (e: Error) => void;
timeout: ReturnType<typeof setTimeout>;
};
const pending = new Map<string, Waiter>();
function waitForMessage(id: string, ms = 5000) {
const { promise, resolve, reject } =
Promise.withResolvers<Message>();
const timeout = setTimeout(() => {
pending.delete(id);
reject(new Error(Message timeout: ${id}));
}, ms);
pending.set(id, { resolve, reject, timeout });
return promise.finally(() => {
clearTimeout(timeout);
pending.delete(id);
});
}Что важно
У promise есть owner -
waitForMessage(). Есть timeout, очистка через finally(), а resolve/reject не утекают наружу. Иначе Map будет удерживать замыкания, таймеры и payload.Практический совет: если у вызывающего кода есть свой lifecycle, добавьте
AbortSignal и снимайте listener в finally().Вывод:
Promise.withResolvers() стоит использовать только там, где явно определены owner, timeout/cancel path и очистка ресурсов.
