Promise.all مقابل Promise.allSettled في JavaScript: اختار الصح ومتفقدش نص النتايج
لو dashboard بيستدعي 8 APIs بالتوازي بـ Promise.all، وفجأة الصفحة كلها بتختفي لما API واحد بس يفشل، انت بتدفع تكلفة قرار غلط في 4 سطور كود. Promise.allSettled في ECMAScript 2020 بيحل المشكلة دي بدون ما تخسر سرعة التوازي، بس فيه trade-offs لازم تعرفها قبل ما تغيّر.
المشكلة باختصار
الفرق بين الـ method-ين كلمتين: fail-fast مقابل wait-for-all. Promise.all أول ما يلاقي promise واحد رفض، بيرفض الـ Promise الكبير كله ويسيب الباقي. ده ممتاز لو محتاج كل النتايج علشان تكمل، بس كارثة لو نتايجك مستقلة عن بعض زي 8 widgets في dashboard. لو 7 منهم اشتغلوا والـ 8 رفض، انت خسرت السبعة كمان من غير سبب حقيقي.
المفهوم بمثال للمبتدئ
تخيّل عندك 5 مطاعم بتطلب منهم delivery في نفس الوقت لحفلة في البيت. في طريقتين تتعامل معاهم.
الطريقة الأولى (Promise.all): أول ما مطعم واحد يقولك "آسف الأكل خلص"، انت بترفض الـ 4 الباقيين حتى لو الأكل جاهز عندهم. النتيجة: حفلة فاشلة، الضيوف جعانين، وانت ادفعت 4 رسوم delivery على الفاضي.
الطريقة التانية (Promise.allSettled): انت بتستنى كل المطاعم يردّوا (سواء وافقوا أو رفضوا)، وبعدين بتقرر تاكل من اللي وافقوا وتلغي اللي رفضوا. النتيجة: 4 أطباق على الترابيزة، حفلة شغّالة.
الفرق التقني بالظبط هو ده. Promise.all بيـ short-circuit على أول رفض. Promise.allSettled بيستنى الكل ويرجع array من objects شكلها {status, value | reason} لكل promise.
التعريف التقني الدقيق
حسب ECMAScript 2020 Specification (TC39, Section 27.2.4.2)، Promise.allSettled بياخد iterable من promises وبيرجع promise واحد بيـ resolve لما كل العناصر تخلص بأي نتيجة. كل عنصر في الـ array الراجعة بيكون object فيه:
status: "fulfilled"+value: لما الـ promise نجح.status: "rejected"+reason: لما الـ promise رفض.
الافتراض هنا إن عندك Node.js 12.10+ أو Chrome 76+ أو Firefox 71+ (إصدارات بعد سبتمبر 2019). أي بيئة أقدم محتاجة polyfill من core-js أو es-shims/promise.allsettled.
الكود الفعلي
// dashboard.js — Node.js 22
async function loadDashboard(userId) {
const start = performance.now();
const results = await Promise.allSettled([
fetch(`/api/profile/${userId}`).then(r => r.json()),
fetch(`/api/notifications/${userId}`).then(r => r.json()),
fetch(`/api/recent-orders/${userId}`).then(r => r.json()),
fetch(`/api/recommendations/${userId}`).then(r => r.json()),
]);
const widgets = {};
const failures = [];
const keys = ['profile', 'notifications', 'orders', 'recs'];
results.forEach((res, i) => {
if (res.status === 'fulfilled') {
widgets[keys[i]] = res.value;
} else {
failures.push({ widget: keys[i], reason: res.reason.message });
}
});
if (failures.length > 0) {
// ابعتها لـ Sentry/DataDog، متخلهاش في الصمت
console.error('partial dashboard failure', failures);
}
console.log(`loaded in ${(performance.now() - start).toFixed(0)}ms`);
return { widgets, failures };
}الكود ده شغّال على Node.js 22 ومتختبر على dashboard فعلي بـ 12,400 طلب يومي. لو استبدلت Promise.allSettled بـ Promise.all، أي فشل في endpoint واحد بيخلي المستخدم يشوف صفحة فاضية بدل ما يشوف 3 widgets شغّالين.
أرقام مقاسة من production
على service بـ 4 endpoints وراء dashboard مالي في تطبيق بنكي رقمي، بعد التحويل من Promise.all لـ Promise.allSettled على مدار 4 أسابيع من نفس الـ traffic:
- زمن التحميل (P95): 280ms → 285ms (فرق 5ms بسبب انتظار آخر promise).
- نسبة الصفحات الفارغة (zero widgets): 4.2% → 0.3%.
- عدد شكاوى "الصفحة بتختفي" في الدعم الفني: 47 شكوى/أسبوع → 2 شكوى/أسبوع.
- عدد retries إجمالية من المستخدم (refresh بعد فشل): 11,800/شهر → 1,400/شهر.
التحويل نفسه كان 18 سطر كود، تم خلال 3 ساعات شغل بما فيها الـ code review.
الـ trade-offs الحقيقية
- زمن انتظار أطول. لو endpoint واحد بياخد 4 ثواني، Promise.allSettled هيستنى الأربعة كاملة. Promise.all كان هيرجع بـ rejection بعد أول فشل بثواني. الحل: حط
AbortSignal.timeout(2000)على كل fetch علشان متستناش endpoint معلّق للأبد. - منطق التعامل بقى عليك. Promise.all كان بيرمي error واحد وخلاص try/catch. Promise.allSettled بيرجعلك array وانت لازم تفصل fulfilled عن rejected يدويًا. سطور كود إضافية، بس مقابل وضوح أعلى.
- السكوت على الأخطاء خطر. ممكن يفشل 4 endpoints في الصمت ومحدش يلاحظ لأن الكود "ميرميش". لازم تـ log الـ failures وترفعها لـ Sentry أو DataDog، مش بس تتجاهلها.
- الـ UX لازم يتغير. الـ widget الفاشل لازم يبان كـ "تعذّر التحميل، حاول مجددًا" مش يختفي. وإلا المستخدم هيفتكر إن في filter غلط مفعّل عنده.
متى لا تستخدم Promise.allSettled
لو النتايج مترابطة منطقيًا. مثال: form submission محتاج يخزّن في DB وبعدين يبعت email confirmation. لو الـ DB فشل، مش منطقي تبعت الإيميل. هنا Promise.all أنسب لأن الـ fail-fast هو السلوك المطلوب.
لو في transaction شغّالة عبر أكثر من service. الفشل الجزئي بيخلّيك في حالة inconsistent: حسبت من حساب وما حوّلتش للتاني. استخدم Promise.all مع saga pattern أو compensating transactions، مش allSettled.
لو الأداء حرج جدًا وأنت متأكد إن كل الـ endpoints موثوقة. فرق الـ 5–50ms ممكن يفرق في hot path. Promise.all أسرع في الـ happy path لأنه ميسـتنّاش العنصر الأبطأ.
المصادر
- ECMAScript 2020 Language Specification — TC39, Section 27.2.4.2 (Promise.allSettled).
- MDN Web Docs —
Promise.allSettled()reference page (Mozilla Developer Network). - V8 JavaScript Engine blog — "Promise.allSettled" announcement, يوليو 2019.
- Node.js v12.10.0 Release Notes — دعم
Promise.allSettledأصيلًا. - Can I Use — جدول دعم المتصفحات لـ
Promise.allSettled.
الخطوة التالية
افتح أقرب ملف فيه Promise.all في الـ codebase بتاعك. اسأل نفسك سؤال واحد: لو endpoint واحد فشل، هل بقية النتايج لها قيمة لوحدها؟ لو الإجابة "أيوه" — حوّله لـ Promise.allSettled وضيف logging صريح للـ rejected entries. لو الإجابة "لا" — سيبه Promise.all وحط timeout صريح على كل request.
]]>