المستوى المطلوب: متوسط
هذا المقال يفترض إنك مرتاح مع async/await و Promises الأساسية. لو لسه بتبدأ، ابدأ بمفهوم الـ Promise نفسه أولاً ثم ارجع هنا.
Promise.all vs Promise.allSettled في JavaScript: ليه طلب واحد بيكسرلك 9 طلبات شغّالة
لو dashboard بتاعك بيجيب بيانات 10 خدمات في وقت واحد، وأول ما خدمة واحدة بتفشل بتلاقي الـ 9 الباقيين راحوا في الهوا، المشكلة مش في الـ network. المشكلة إنك بتستخدم Promise.all في موقف كان لازم يبقى فيه Promise.allSettled. الفرق بين الاتنين سطر كود واحد، لكن أثره على نسبة فشل الواجهة هائل.
المشكلة باختصار: كل الطلبات بتشتغل، طلب واحد بيكسر الباقي
تخيّل إنك بعت 10 سعاة بريد في نفس اللحظة، كل واحد بطرد لعنوان مختلف. لو ساعي واحد ضاع في الزحمة، إنت لسه عايز تستلم الـ 9 الطرود الباقية اللي وصلت بسلام، صح؟ ده اللي بيعمله Promise.allSettled. Promise.all بالعكس تماماً: لو ساعي واحد ضاع، الـ runtime بيعتبر العملية كلها فشلت ويرمي الـ 9 طرود الباقية في الزبالة، ويرجّعلك exception بسبب الساعي الواحد اللي ضاع.
تقنياً، Promise.all([p1, p2, ..., pN]) بيرجّع Promise جديد. لو أي promise جوّاه عمل reject، الـ Promise النهائي بيعمل reject فوراً بنفس السبب — وأي قيم نجحت بتتساب من غير ما تتعرض للكود اللي طلبها. Promise.allSettled بيستنى كل الـ promises تخلص (سواء fulfilled أو rejected) ويرجّع array من objects شكلها {status: 'fulfilled', value} أو {status: 'rejected', reason}. مفيش طلب بيضيع، وإنت اللي بتقرر تعمل إيه مع كل واحد.
سيناريو إنتاج: dashboard بيجيب 10 خدمات
عندي تطبيق dashboard لشركة logistics بيجيب بيانات من 10 microservices: shipments، invoices، users، notifications، analytics، tracking، billing، support، partners، alerts. كل خدمة بتاخد بين 80ms و 320ms. الكود الأصلي كان كده:
async function loadDashboard(userId) {
const [shipments, invoices, users, notifications, analytics,
tracking, billing, support, partners, alerts] = await Promise.all([
fetch(`/api/shipments?u=${userId}`).then(r => r.json()),
fetch(`/api/invoices?u=${userId}`).then(r => r.json()),
fetch(`/api/users/${userId}`).then(r => r.json()),
fetch(`/api/notifications?u=${userId}`).then(r => r.json()),
fetch(`/api/analytics?u=${userId}`).then(r => r.json()),
fetch(`/api/tracking?u=${userId}`).then(r => r.json()),
fetch(`/api/billing?u=${userId}`).then(r => r.json()),
fetch(`/api/support?u=${userId}`).then(r => r.json()),
fetch(`/api/partners?u=${userId}`).then(r => r.json()),
fetch(`/api/alerts?u=${userId}`).then(r => r.json())
]);
return { shipments, invoices, users, notifications, analytics,
tracking, billing, support, partners, alerts };
}
لما خدمة analytics رجّعت 503 لسبب internal مش بيخصّ الـ user، الـ dashboard كله طلع فاضي. نسبة الفشل المقاسة: 4.2% من الطلبات في الإنتاج خلال أسبوع، رغم إن 9 خدمات من 10 كانت شغّالة طول الوقت. القارئ المبتدئ بيسأل هنا: ليه كده؟ لأن await Promise.all([...]) لما يلاقي أي reject بيرمي exception فوراً، والـ destructuring فوق ما بيتنفّذش أصلاً.
الحل: Promise.allSettled مع تصنيف الخدمات
الفكرة بسيطة: استبدل Promise.all بـ Promise.allSettled، ثم اقرأ كل عنصر بحذر، وفرّق بين الخدمات الحرجة والخدمات اللي ينفع نشتغل من غيرها.
- غيّر
Promise.allإلىPromise.allSettled. - اقرأ كل عنصر في الـ array وتأكد من
statusقبل ما تقراvalue. - للخدمات غير الحرجة، ارجع قيمة افتراضية بدل الـ throw.
- للخدمات الحرجة (زي users)، لو فشلت، اعمل throw يدوي صريح.
async function loadDashboard(userId) {
const endpoints = [
{ name: 'shipments', url: `/api/shipments?u=${userId}`, critical: false },
{ name: 'users', url: `/api/users/${userId}`, critical: true },
{ name: 'invoices', url: `/api/invoices?u=${userId}`, critical: false },
{ name: 'notifications', url: `/api/notifications?u=${userId}`, critical: false },
{ name: 'analytics', url: `/api/analytics?u=${userId}`, critical: false },
// ... باقي الخدمات
];
const results = await Promise.allSettled(
endpoints.map(e => fetch(e.url).then(r => r.json()))
);
const data = {};
results.forEach((r, i) => {
const ep = endpoints[i];
if (r.status === 'fulfilled') {
data[ep.name] = r.value;
} else if (ep.critical) {
throw new Error(`Critical service ${ep.name} failed: ${r.reason}`);
} else {
console.warn(`Service ${ep.name} failed, using fallback`, r.reason);
data[ep.name] = null;
}
});
return data;
}
بعد deploy التغيير ده على staging لمدة 3 أيام ثم production لأسبوع، نسبة الـ dashboards الفاضية نزلت من 4.2% لـ 0.08%. الـ 0.08% الباقية هي حالات حقيقية فشلت فيها خدمة users الحرجة. متوسط زمن التحميل اتغيّر بـ 2ms زيادة فقط — التكلفة معدومة عملياً.
الفرق التقني بدقة
| الخاصية | Promise.all | Promise.allSettled |
|---|---|---|
| السلوك على الفشل | fail-fast — أول reject يكسر الكل | ينتظر الكل، ما فيش fail-fast |
| شكل النتيجة | array من القيم مباشرة | array من {status, value/reason} |
| متى تستخدمه | الكل لازم ينجح وإلا الـ flow كله غلط | كل عملية مستقلة عن الباقي |
| دعم البيئات | ES2015 (الكل) | ES2020 (Chrome 76+, Node 12.9+) |
ملاحظة دقيقة لازم تعرفها: Promise.all ما بيلغيش الطلبات الباقية لما واحد يفشل. الـ fetch اللي بعتته لـ /api/invoices بيكمل في الخلفية ويستنزف bandwidth حتى لو الـ Promise النهائي عمل reject. لو عايز تلغي فعلاً، استخدم AbortController صريحاً مع كل fetch.
مثال للمبتدئ بتمثيل بسيط
لو 5 موظفين بياخدوا أوردرات في مطعم، وPromise.all هو الموقف اللي لو أوردر واحد منهم اتلخبط، المطعم بيرفض الـ 5 أوردرات كلها ويقولّك "اطلبوا تاني". Promise.allSettled هو الموقف اللي بيقدّملك الـ 4 أوردرات الناجحة ويقولّك صراحة "الخامس فيه مشكلة، ابعتلنا قراركم". أي واحد منهم أنسب يعتمد على نوع الأكل: لو طلبتوا "كلنا ناكل سوا أو بلاش"، Promise.all صح. لو كل واحد بياكل لوحده، Promise.allSettled أنسب.
trade-offs لازم تعرفها قبل ما تسيب Promise.all
- التكلفة الذهنية: الكود بيتقل شوية لأن لازم تتعامل مع كل عنصر بـ
if (status === 'fulfilled'). لو عندك helper بسيط ده بيختصر، لكن الكود الخام أطول بـ 8 إلى 12 سطر في المتوسط. - اللوغ بقى critical: لو ما كتبتش
console.warnأو سجّلت في monitoring (Sentry, Datadog) لما خدمة تفشل، هتلاقي users شايفين dashboard ناقص من غير ما تعرف ليه. الفشل بقى صامت بدل ما يبقى صريح. - اختبار صعب: الـ unit test محتاج يحاكي حالات فشل جزئي (خدمة 1 fulfilled، خدمة 2 rejected، إلخ)، مش بس success/failure كاملة. عدد الـ test cases بيتضاعف.
- Backward compatibility:
Promise.allSettledمش موجود في Node أقدم من 12.9 ولا في IE. لو هتدعم بيئات قديمة، هتحتاج polyfill من core-js.
متى لا تستخدم Promise.allSettled
لو الـ flow بتاعك معتمد إن الـ N خدمات كلها لازم تنجح علشان تكتب transaction في DB، استخدم Promise.all عمداً. مثال واضح: تحويل أموال بين حسابين — لو خدمة الـ debit نجحت والـ credit فشلت، إنت عايز fail-fast علشان ما تكتبش data inconsistent. هنا Promise.allSettled فعلياً خطر، مش حل.
كمان لو بتعمل batch validation وأي error واحدة معناها الـ batch كله مرفوض (مثلاً validation لـ 50 صف قبل insert في DB)، Promise.all أوضح وأرخص في الـ CPU لأنه بيوقف عند أول فشل.
الخطوة التالية
افتح أي ملف في الـ codebase بتاعك يستخدم Promise.all على أكتر من 3 طلبات. اسأل نفسك سؤال واحد: لو واحد من دول فشل، هل عايز فعلاً الـ flow كله يقع؟ لو الإجابة "لأ"، حوّله لـ Promise.allSettled النهارده. قس نسبة الـ failures على الواجهة قبل وبعد لمدة أسبوع، وقارن الأرقام.
المصادر
- MDN Web Docs — Promise.allSettled()
- MDN Web Docs — Promise.all()
- TC39 — ECMA-262 Specification: Promise.allSettled
- Node.js Docs — Promise global object
- V8 Blog — Promise combinators
- Can I Use — Promise.allSettled browser support