Promise.withResolvers — создание промисов с внешним управлением

10 сентября 2026
Автор

Антон Ларичев

Что такое Promise.withResolvers

Promise.withResolvers() — статический метод, добавленный в спецификацию ECMAScript 2024. Он позволяет создать промис вместе с его функциями resolve и reject, возвращая их единым объектом. Это решает распространённую задачу: управлять промисом из-за пределов колбэка конструктора.

Курс по теме

Курс JavaScript с нуля

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Проблема, которую решает метод

До появления Promise.withResolvers разработчики использовали паттерн «deferred» — объявляли переменные для resolve и reject снаружи конструктора:

let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

Код работает, но многословен и неочевиден: переменные объявлены как let, хотя после первого присваивания никогда не меняются. Часто разработчики оборачивали это в вспомогательную функцию createDeferred(). Promise.withResolvers() встраивает эту логику прямо в стандарт.

Синтаксис и возвращаемое значение

const { promise, resolve, reject } = Promise.withResolvers();

Метод не принимает аргументов и возвращает объект с тремя свойствами:

  • promise — новый объект Promise в состоянии pending
  • resolve — функция перевода промиса в состояние fulfilled
  • reject — функция перевода промиса в состояние rejected

Базовые примеры

Разрешение промиса извне

const { promise, resolve } = Promise.withResolvers();

setTimeout(() => resolve('Данные загружены'), 1000);

promise.then(value => console.log(value)); // "Данные загружены"

Отклонение промиса

const { promise, reject } = Promise.withResolvers();

setTimeout(() => reject(new Error('Ошибка сети')), 500);

promise.catch(err => console.error(err.message)); // "Ошибка сети"

Практические сценарии

Ожидание события

Паттерн особенно полезен при работе с EventEmitter или DOM-событиями, где промис нужно разрешить из обработчика:

function waitForEvent(emitter, eventName) {
  const { promise, resolve, reject } = Promise.withResolvers();

  emitter.once(eventName, resolve);
  emitter.once('error', reject);

  return promise;
}

// Node.js EventEmitter
const stream = fs.createReadStream('file.txt');
await waitForEvent(stream, 'end');
console.log('Файл прочитан');
// DOM-событие
function waitForClick(element) {
  const { promise, resolve } = Promise.withResolvers();
  element.addEventListener('click', resolve, { once: true });
  return promise;
}

const button = document.querySelector('#submit');
await waitForClick(button);
console.log('Кнопка нажата');

Отменяемый запрос

Promise.withResolvers упрощает реализацию операций с возможностью отмены:

function createCancellableRequest(url) {
  const { promise, resolve, reject } = Promise.withResolvers();
  const controller = new AbortController();

  fetch(url, { signal: controller.signal })
    .then(response => response.json())
    .then(resolve)
    .catch(reject);

  return {
    promise,
    cancel: () => {
      controller.abort();
      reject(new Error('Запрос отменён'));
    }
  };
}

const { promise, cancel } = createCancellableRequest('https://api.example.com/data');

const timeout = setTimeout(cancel, 3000);

promise
  .then(data => {
    clearTimeout(timeout);
    console.log('Данные:', data);
  })
  .catch(err => console.error(err.message));

Очередь задач

При построении очереди с контролируемым выполнением каждая задача возвращает промис, который разрешается по завершении:

class TaskQueue {
  constructor() {
    this.queue = [];
    this.processing = false;
  }

  enqueue(task) {
    const { promise, resolve, reject } = Promise.withResolvers();
    this.queue.push({ task, resolve, reject });

    if (!this.processing) {
      this.process();
    }

    return promise;
  }

  async process() {
    this.processing = true;

    while (this.queue.length > 0) {
      const { task, resolve, reject } = this.queue.shift();

      try {
        const result = await task();
        resolve(result);
      } catch (error) {
        reject(error);
      }
    }

    this.processing = false;
  }
}

const queue = new TaskQueue();

const r1 = queue.enqueue(() => fetchUserData(1));
const r2 = queue.enqueue(() => fetchUserData(2));
const r3 = queue.enqueue(() => fetchUserData(3));

// Задачи выполняются последовательно, результаты доступны параллельно
const [user1, user2, user3] = await Promise.all([r1, r2, r3]);

WebSocket с запрос-ответ

При двусторонней связи через WebSocket каждый отправленный запрос ожидает конкретный ответ по идентификатору:

class WebSocketClient {
  constructor(url) {
    this.socket = new WebSocket(url);
    this.pending = new Map();

    this.socket.addEventListener('message', (event) => {
      const { id, data, error } = JSON.parse(event.data);

      if (this.pending.has(id)) {
        const { resolve, reject } = this.pending.get(id);
        this.pending.delete(id);

        if (error) {
          reject(new Error(error));
        } else {
          resolve(data);
        }
      }
    });
  }

  send(payload) {
    const id = crypto.randomUUID();
    const { promise, resolve, reject } = Promise.withResolvers();

    this.pending.set(id, { resolve, reject });
    this.socket.send(JSON.stringify({ id, ...payload }));

    return promise;
  }
}

const client = new WebSocketClient('wss://api.example.com/ws');
const user = await client.send({ type: 'getUser', userId: 42 });
console.log('Пользователь:', user);

Тайм-аут для произвольного промиса

function withTimeout(promise, ms) {
  const { promise: timeoutPromise, reject } = Promise.withResolvers();

  const timer = setTimeout(
    () => reject(new Error(`Превышено время ожидания: ${ms} мс`)),
    ms
  );

  return Promise.race([
    promise.finally(() => clearTimeout(timer)),
    timeoutPromise
  ]);
}

try {
  const data = await withTimeout(fetchData(), 5000);
  console.log(data);
} catch (err) {
  console.error(err.message); // "Превышено время ожидания: 5000 мс"
}

Диалог с ожиданием подтверждения

Паттерн полезен, когда нужно дождаться действия пользователя:

class ConfirmationDialog {
  constructor() {
    const { promise, resolve, reject } = Promise.withResolvers();
    this.promise = promise;
    this._resolve = resolve;
    this._reject = reject;
  }

  confirm() {
    this._resolve(true);
  }

  cancel() {
    this._resolve(false);
  }

  dismiss() {
    this._reject(new Error('Диалог закрыт принудительно'));
  }
}

const dialog = new ConfirmationDialog();
showModal('Удалить файл?', {
  onConfirm: () => dialog.confirm(),
  onCancel: () => dialog.cancel(),
  onDismiss: () => dialog.dismiss(),
});

const confirmed = await dialog.promise;
if (confirmed) {
  await deleteFile();
}

Сравнение с традиционным подходом

До ES2024 — вспомогательная функция:

function createDeferred() {
  let resolve, reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}

const { promise, resolve, reject } = createDeferred();

ES2024 — встроенный метод:

const { promise, resolve, reject } = Promise.withResolvers();

Функциональность идентична. Встроенный метод не требует вспомогательного кода и сразу понятен читателю, знакомому со стандартом.

Совместимость и полифилл

Метод поддерживается в актуальных окружениях:

  • Chrome 119+
  • Firefox 121+
  • Safari 17.4+
  • Node.js 22+

Для более старых окружений достаточно простого полифилла:

if (!Promise.withResolvers) {
  Promise.withResolvers = function () {
    let resolve, reject;
    const promise = new Promise((res, rej) => {
      resolve = res;
      reject = rej;
    });
    return { promise, resolve, reject };
  };
}

Важные нюансы

Промис разрешается только один раз

Как и в обычном промисе, первый вызов resolve или reject фиксирует итоговое состояние. Все последующие вызовы игнорируются:

const { promise, resolve, reject } = Promise.withResolvers();

resolve('первое значение');
resolve('второе значение'); // игнорируется
reject(new Error('ошибка')); // тоже игнорируется

promise.then(v => console.log(v)); // "первое значение"

Промис может остаться в pending навсегда

Если не вызвать ни resolve, ни reject, промис останется в состоянии pending. Это приводит к утечкам памяти, поэтому важно предусмотреть все пути завершения, включая обработку ошибок:

function loadData(url) {
  const { promise, resolve, reject } = Promise.withResolvers();

  fetch(url)
    .then(r => r.json())
    .then(resolve)
    .catch(reject); // без этой строки промис зависнет при ошибке fetch

  return promise;
}

resolve и reject можно передавать как колбэки

Функции не привязаны к контексту вызова, поэтому их можно передавать напрямую туда, где ожидается колбэк:

const { promise, resolve } = Promise.withResolvers();

// Прямая передача как колбэк — без обёртки в стрелочную функцию
setTimeout(resolve, 1000, 'готово');

promise.then(console.log); // "готово"

Заключение

Promise.withResolvers() — небольшое, но ценное дополнение к стандартному API. Метод устраняет необходимость в самодельных вспомогательных функциях и делает код с «внешним» управлением промисом чище и понятнее. Сценарии применения широки: ожидание событий, построение очередей, реализация WebSocket-клиентов, добавление тайм-аутов. Везде, где промис нужно разрешить из кода, внешнего по отношению к исполнителю задачи, этот метод — правильный инструмент.

Для глубокого изучения промисов, async/await и всей асинхронной модели JavaScript приглашаем на курс PurpleSchool: https://purpleschool.ru/course/javascript?utm_source=knowledgebase&utm_medium=text&utm_campaign=promise-with-resolvers

Постройте личный план изучения Javascript до уровня Middle — бесплатно!

Javascript — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Javascript

Как работает метод trim() - JavaScriptКак работает метод toUpperCase() - JavaScriptКак работает метод toLowerCase() - JavaScriptКак работает метод substring() - JavaScriptКак работает метод startsWith() - JavaScriptКак работает метод split() - JavaScriptКак работает метод slice() - JavaScriptКак работает метод search() - JavaScriptКак работает метод replaceAll() - JavaScriptКак работает метод replace() - JavaScriptКак работает метод repeat() - JavaScriptКак работает метод padStart() - JavaScriptКак работает метод padEnd() - JavaScriptКак работает метод matchAll() - JavaScriptКак работает метод match() - JavaScriptКак работает метод localeCompare() - JavaScriptКак работает свойство length - JavaScriptКак работает метод lastIndexOf() - JavaScriptКак работает метод indexOf() - JavaScriptКак работает метод includes() - JavaScriptКак работает метод fromCodePoint() - JavaScriptКак работает метод fromCharCode() - JavaScriptКак работает метод endsWith() - JavaScriptКак работает метод concat() - JavaScriptКак работает метод codePointAt() - JavaScriptКак работает метод charCodeAt() - JavaScriptКак работает метод charAt() - JavaScript
Итератор в JavaScript
try...catch в JavaScriptError в JavaScript
Событие wheel в JavaScriptСобытие unload в JavaScriptСобытие touch в JavaScriptСобытие submit в JavaScriptСобытие scroll в JavaScriptСобытие reset в JavaScriptМетод .preventDefault() в JavaScriptСобытие mouseover в JavaScriptСобытие mouseout в JavaScriptСобытие load в JavaScriptСобытие keyup в JavaScriptСобытие keydown в JavaScriptСобытие invalid в JavaScriptСобытие input в JavaScriptСобытийная модель Event в JavaScriptОбъект события Event в JavaScriptСобытие DOMContentLoaded в JavaScriptСобытие dblclick в JavaScriptСобытие click в JavaScriptСобытие change в JavaScriptJavaScript BroadcastChannel — межвкладочное взаимодействиеBroadcast Channel API в JavaScriptСобытие beforeunload в JavaScript
Error cause — цепочки ошибок в JavaScript
Методы массивов toSorted, toReversed и withКак работает метод some() - JavaScriptКак работает метод reverse() - JavaScriptКак работает метод reduce() - JavaScriptКак работает метод map() - JavaScriptКак работает метод isArray() - JavaScriptКак работает метод indexOf() - JavaScriptКак работает метод includes() - JavaScriptКак работает метод from() - JavaScriptКак работает метод forEach() - JavaScriptКак работает метод flatMap() - JavaScriptКак работает метод flat() - JavaScriptКак работает метод findIndex() - JavaScriptКак работает метод find() - JavaScriptКак работает метод filter() - JavaScriptКак работает метод every() - JavaScriptМассивы в JavaScriptArray.at, findLast, findLastIndex — новые методы массивов
Открыть базу знаний

Лучшие курсы по теме

изображение курса

Основы JavaScript

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий