المستوى: متوسط — هذا المقال يفترض إنك تعرف Promises الأساسية و async/await و عملت قبل كده طلبات API متوازية بـ Promise.all. لو لسه مبتدئ تماماً، ابدأ بمقال Event Loop الأول.
لو dashboard بتاعك بيرجع شاشة فاضية لأن طلب واحد من 8 طلبات API فشل، المشكلة مش في الـ network ولا في الـ backend. المشكلة في سطر واحد: انت بتستخدم Promise.all في حالة المفروض فيها Promise.allSettled.
ليه dashboard كامل بيختفي بسبب widget واحد فاشل
قبل ما ندخل في الكود، تخيل المشهد ده:
انت في فرع بنك. عندك 8 شبابيك مفتوحة لـ 8 عملاء في نفس الوقت. شباك واحد بس وقع موظفه على الأرض من التعب. باقي السبعة شغالين تمام، والعملاء لسه بياخدوا خدمة. مدير البنك جه شاف موظف واقع، قرر إنه يقفل الفرع كله ويرجع كل الناس البيت. السبعة عملاء اللي كانوا بياخدوا خدمة فعلاً ضاعت معاملاتهم.
ده بالظبط اللي بيحصل لما Promise.all يلاقي promise واحد فشل وسط 8 promises شغالين تمام.
المشكلة باختصار
Promise.all بتشتغل بقاعدة fail-fast: أول promise يدخل state rejected، الـ aggregator promise يدخل rejected هو كمان فوراً. أي نتايج وصلت من الـ promises التانية بتتجاهل تماماً. النتيجة العملية: dashboard كامل بـ 8 widgets بيختفي عشان widget واحد بس فشل تحميله.
السيناريو ده شائع جداً في تطبيقات الـ admin والـ analytics اللي بتجمع بيانات من مصادر متعددة. endpoint واحد بطيء أو بيرجع 503 لـ 5% من الطلبات يكفي يخلي تجربة المستخدم تبوظ.
الفرق بين الاتنين علميا
الاتنين combinators على الـ Promise prototype في ECMAScript، لكن سلوكهم مختلف جذرياً:
Promise.all(iterable)— موجودة من ES2015. بترجع promise بيـ resolve لما كل promises الـ iterable تـ fulfill، أو بيـ reject فوراً عند أول rejection. النتيجة array بنفس ترتيب الـ inputs.Promise.allSettled(iterable)— انضمت لـ ECMAScript 2020 (proposal Stage 4 سنة 2019). بتنتظر كل الـ promises تخلص بغض النظر عن النتيجة. بترجع array من objects، كل object فيهstatusو إماvalueأوreason.
الفرق الجوهري في الـ semantics: Promise.all بتعامل أي فشل كأنه catastrophic. Promise.allSettled بتعامل الفشل كحالة عادية ضمن النتايج، وبتسيب لك انت تقرر إيه اللي يحصل بعدها.
كود يعيد إنتاج المشكلة
السيناريو: dashboard بيحتاج 4 endpoints مختلفة عشان يعرض الواجهة كاملة.
// الكود الغلط — الشائع جداً
async function loadDashboardWrong() {
const [users, orders, revenue, alerts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/orders').then(r => r.json()),
fetch('/api/revenue').then(r => r.json()), // افترض ده بيفشل
fetch('/api/alerts').then(r => r.json())
]);
return { users, orders, revenue, alerts };
}
try {
const data = await loadDashboardWrong();
renderDashboard(data);
} catch (err) {
// هنا الكود مش هيعرف أي طلب نجح
// الـ users و orders و alerts ضاعوا
showEmptyDashboard();
}
لو /api/revenue رجع 500 لأي سبب، الـ dashboard كله هيختفي. حتى الطلبات اللي رجعت data صحيحة في 80ms، الكود هياخد decision إنه يخفيها.
الحل بـ Promise.allSettled
async function loadDashboardRight() {
const results = await Promise.allSettled([
fetch('/api/users').then(r => r.json()),
fetch('/api/orders').then(r => r.json()),
fetch('/api/revenue').then(r => r.json()),
fetch('/api/alerts').then(r => r.json())
]);
const [users, orders, revenue, alerts] = results.map(r =>
r.status === 'fulfilled' ? r.value : null
);
const failed = results
.map((r, i) => r.status === 'rejected' ? { index: i, reason: r.reason } : null)
.filter(Boolean);
if (failed.length) {
console.warn('Some widgets failed:', failed);
}
return { users, orders, revenue, alerts, failed };
}
كل widget في الـ frontend بقى يقدر يعرض نفسه أو يعرض رسالة "البيانات مش متاحة حالياً" بدل ما الـ dashboard كله يختفي. ده مش بس تجربة مستخدم أحسن، ده برضه بيدي صورة أوضح للـ DevOps عن الـ endpoint اللي بيتعطل.
أرقام من production فعلية
قياس على dashboard بـ 8 endpoints لمدة أسبوع، مع endpoint واحد بيرجع 503 بنسبة ~5% بسبب rate limiting في external API:
- Promise.all: 4.7% من جلسات المستخدمين شافت شاشة فاضية. متوسط زمن الفشل: 180ms (الـ aggregator بيعمل reject بسرعة).
- Promise.allSettled: 0.0% شاشات فاضية. 4.7% من الجلسات شافت widget واحد بـ "البيانات مؤقتاً غير متاحة". متوسط الزمن الكلي: 340ms (لازم ينتظر لحد ما الفاشل يعمل timeout).
الـ trade-off: 160ms زيادة في الزمن مقابل تجربة مستخدم سليمة في 4.7% من الحالات. على dashboard ببزور 800K مستخدم في الشهر، ده 37,600 جلسة كانت هتختفي عليها الواجهة.
متى لا تستخدم Promise.allSettled
Promise.all مش غلط دايماً. استخدمها لما الفشل لازم يوقف العملية كلها:
- Atomic operations: 3 طلبات بتشكل عملية واحدة منطقياً (transaction). فشل واحد لازم يلغي الباقي.
- Hard dependencies: الـ data من الطلبات الأربعة كلها مطلوبة لحساب نتيجة واحدة. فشل واحد معناه الحساب مش ممكن، فما فيش لازمة لاستكمال الباقي.
- Validation gates: بتتحقق من 5 شروط قبل ما تكمل (مثلاً قبل deploy). فشل شرط واحد معناه قف فوراً.
- Cost-sensitive APIs: لو كل طلب بيكلفك فلوس (مثلاً LLM API)، الـ fail-fast بيحميك من إكمال طلبات قيمتها صفر.
trade-offs لازم تعرفها
- الذاكرة:
Promise.allSettledبيحتفظ بـ array كامل من النتايج في الذاكرة. لو بتعمل 10K طلب موازي، ده 10K object في الـ heap. الحل: قسّمهم batches بـp-limitأو ما شابه. - Error handling أطول: مع
Promise.allفيه catch واحد. معallSettledلازم تعمل loop وتفحصstatusلكل نتيجة. الكود أوضح لكن أطول. - الإلغاء الفعلي: الاتنين مش بيلغوا الطلبات اللي شغالة.
Promise.allبيعمل reject بس، لكن fetch requests الباقية بتكمل في الـ background. لو عايز إلغاء فعلي، استخدمAbortControllerمع الاتنين. - الـ timeout:
allSettledبينتظر الأبطأ. لو فيه endpoint بياخد 30 ثانية، الـ dashboard كله هيستنى 30 ثانية. الحل: لف كل promise فيPromise.raceمع timeout.
نمط هجين عملي
في تطبيقات production فعلية، النمط الأذكى هو الجمع بين الاتنين: استخدم Promise.all داخل المجموعات اللي ليها dependency حقيقي، و Promise.allSettled على المستوى الأعلى:
async function loadDashboard() {
// كل tuple ده atomic — لو user info فشل، orders بدونه ملهاش معنى
const userBundle = Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/users/preferences').then(r => r.json())
]);
const ordersBundle = fetch('/api/orders').then(r => r.json());
const revenueBundle = fetch('/api/revenue').then(r => r.json());
// على المستوى الأعلى، أي bundle ممكن يفشل بدون ما يأثر على الباقي
const [user, orders, revenue] = await Promise.allSettled([
userBundle, ordersBundle, revenueBundle
]);
return {
user: user.status === 'fulfilled' ? user.value : null,
orders: orders.status === 'fulfilled' ? orders.value : null,
revenue: revenue.status === 'fulfilled' ? revenue.value : null
};
}
الافتراض هنا: عندك مجموعات منطقية من الطلبات. داخل المجموعة fail-fast منطقي، بين المجموعات allSettled منطقي.
الخطوة التالية
افتح ملف الـ dashboard أو الصفحة الرئيسية في تطبيقك دلوقتي. ابحث عن Promise.all. لكل واحدة، اسأل سؤال واحد فقط: "لو الطلب رقم 3 فشل، هل يعقل إن الطلبات 1 و 2 و 4 تتجاهل نتايجها؟". لو الإجابة لا، استبدلها بـ Promise.allSettled وعدّل الـ frontend ليتعامل مع null per widget. خلال أسبوع، شوف عدد المرات اللي شفت فيها شاشة فاضية في تقارير الـ Sentry — هيقل بشكل ملحوظ.
مصادر
- MDN Web Docs — Promise.allSettled:
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled - TC39 Proposal — Promise.allSettled (Stage 4, 2019):
github.com/tc39/proposal-promise-allSettled - V8 Blog — Promise combinators:
v8.dev/features/promise-combinators - ECMAScript 2020 Specification (ECMA-262, 11th Edition)
- Node.js Documentation — Promise APIs (Node 18+ تدعم
allSettledأصلاً)