Promise.withResolvers(): بدل نمط let resolve القديم
ليه deferred بـ let resolve متعب، وإزاي Promise.withResolvers() بيرجع promise و resolve و reject في سطر واحد — مع الدعم في المتصفحات و Node 22.
لسه بتعمل deferred promise بالطريقة القديمة؟ let resolve; const p = new Promise(r => { resolve = r }) — نمط متعب وبيخلّي الـ variables تطلع برا الـ executor. من مارس ٢٠٢٤ فيه API رسمي يعمل نفس الحاجة في سطر واحد: Promise.withResolvers().
أسبوع معرض القاهرة للتكنولوجيا، لو رايح قولنا هتحضر إيه 👇 والنهاردة الـ tip عن حاجة بتوفّر nesting كتير في الكود الـ async، وبتخلّي الـ resolve والـ reject في إيدك من غير حِيَل.
المشكلة في الـ deferred القديم
لما الـ resolve والـ reject محتاجين يتنادوا من برّا الـ Promise executor (مثلاً من event listener أو callback قديم أو queue queue)، المطورين كانوا بيعملوا كده:
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
الشغل بيتم، بس فيه let فاضية في الأول، والـ assignment جوّه الـ executor، والنمط ده بيتكرر في كل مشروع. وكمان لو حد قرا الكود بسرعة ممكن يفتكر إن في حاجة غلط عشان الـ variables بتتغيّر بعد التعريف.
الحالة الكلاسيكية: عندك زرار أو WebSocket أو stream، ومش قادر تلفّ كل اللوجيك جوّه الـ executor. ساعتها محتاج الـ resolvers برّا.
الحل: Promise.withResolvers()
حسب MDN، الـ static method دي بترجع plain object فيه تلات خصائص: promise و resolve و reject.
const { promise, resolve, reject } = Promise.withResolvers();
button.addEventListener("click", () => resolve("clicked"));
button.addEventListener("error", (e) => reject(e));
const result = await promise;
الفرق المهم: الـ resolve والـ reject عايشين في نفس الـ scope بتاع الـ promise نفسه، مش محبوسين جوّه الـ executor. وده اللي بيخلّي الـ nesting أقل، وبيسهّل حالات زي الـ streams والـ queues والأحداث المتكررة — تقدر تعيد إنشاء resolvers جديدة في كل دورة من غير ما تلغّبط الـ listeners.
تكافؤ كامل مع النمط القديم
MDN بيقول إن Promise.withResolvers() مكافئ تمامًا للكود القديم اللي فوق، بس أقصر ومش محتاج let. يعني نفس الـ semantics بتاعة Promise() constructor:
- لو ناديْت
resolve(value)الـ promise بيتحلّ بالقيمة - لو ناديْت
reject(reason)بيترفض - النداء التاني بعد ما الـ promise settled بيتحيّد (ignored) زي أي Promise عادي
مفيش سحر إضافي — بس ergonomics أحسن.
دعم المتصفحات و Node
حسب MDN و Browser Compat Data:
- Baseline Widely available من مارس ٢٠٢٤
- Chrome 119+ · Firefox 121+ · Safari 17.4+
- Node.js من v22.0.0 · Deno من 1.38
وبما إن الـ method على Promise نفسه، شغّالة في الـ window والـ workers وأي مكان فيه Promise على الـ globalThis. لو المشروع لسه على Node 20 LTS، محتاج polyfill بسيط أو ترقية — الـ API مش موجودة unflagged قبل v22 حسب BCD.
مثال عملي: انتظار حدث مرة واحدة
function waitForEvent(target, type) {
const { promise, resolve, reject } = Promise.withResolvers();
const onOk = (e) => { cleanup(); resolve(e); };
const onErr = (e) => { cleanup(); reject(e); };
function cleanup() {
target.removeEventListener(type, onOk);
target.removeEventListener("error", onErr);
}
target.addEventListener(type, onOk, { once: true });
target.addEventListener("error", onErr, { once: true });
return promise;
}
const ev = await waitForEvent(window, "load");
ده demo بسيط للـ pattern. في production راجع الـ cleanup والـ timeout وإلغاء الاشتراك حسب السياق — خصوصًا لو الـ target ممكن يتدمّر قبل ما الحدث يحصل.
Bonus: generic و subclassing
الـ method generic: لو عندك subclass من Promise والـ constructor بيقبل executor بنفس التوقيع، تقدر تنادي MyPromise.withResolvers() والنتيجة هتكون promise من نوع الـ subclass. مش هتحتاجها كل يوم، بس مفيدة لو بتبني abstractions أو test helpers فوق الـ Promise.
رأيي
أي كود لسه بيعمل deferred بـ let resolve يستاهل يتبدّل لـ Promise.withResolvers(). أوضح في القراءة، أقصر، وBaseline في المتصفحات الحديثة و Node 22+. متخلّيش النمط القديم يفضل في كود جديد من غير سبب — والـ tip ده من الحاجات اللي أول ما تعرفها مش هترجع للـ boilerplate تاني.
المصادر
- javascript
- promise
- nodejs
- web-apis
- baseline
- fullstack