لو عندك دالة بتعمل 5 طلبات HTTP متوازية، واحد منهم فشل، الـ function كلها بترمي خطأ وكل النتائج التانية بتضيع. ده مش bug في الكود، ده اختيار غلط بين Promise.all و Promise.allSettled.
Promise.all ضد Promise.allSettled: الفرق بالظبط
المشكلة باختصار
الاتنين بياخدوا array من promises ويشغّلوهم بالتوازي. الفرق الحقيقي في سياسة الفشل: Promise.all بترفض فورًا أول ما أي promise يفشل، وبترجع reason بتاع أول فشل بس. Promise.allSettled بتستنى الكل ينتهي، وبترجع array فيه نتيجة كل promise سواء نجح أو فشل. الفرق ده بيحدد هل المستخدم هيشوف data ناقصة ولا شاشة فاضية.
اللي بيحصل فعلاً في كل واحد
Promise.all بتتصرف fail-fast: أول rejection بيحوّل الـ promise الراجعة لحالة rejected، والباقي لسه شغّال لكن نتيجته بترمى. ركز: الطلبات الباقية مش بتتلغى تلقائيًا، الـ network request لسه ماشي، إنت بس مش هتشوف نتيجته. لو عايز تلغي فعلاً، محتاج AbortController يدوي.
Promise.allSettled بتستنى كل promise يوصل لحالة settled (fulfilled أو rejected). بترجع array من objects، كل object فيه status و value أو reason:
[
{ status: "fulfilled", value: { id: 1 } },
{ status: "rejected", reason: Error("timeout") },
{ status: "fulfilled", value: { id: 3 } }
]
سيناريو واقعي بيحصل كل أسبوع
dashboard بيعرض بيانات من 4 مصادر: user profile، notifications، billing، analytics. لو استخدمت Promise.all وخدمة billing وقعت لحظة التحميل، المستخدم شاف شاشة فاضية بدل ما يشوف بياناته الأساسية وplaceholder بسيط مكان الـ billing. ده bug حقيقي بيتكرر في production لأن المبرمج اختار الـ primitive الغلط.
الحل العملي بـ Promise.allSettled:
async function loadDashboard(userId) {
const results = await Promise.allSettled([
fetch(`/api/profile/${userId}`).then(r => r.json()),
fetch(`/api/notifications/${userId}`).then(r => r.json()),
fetch(`/api/billing/${userId}`).then(r => r.json()),
fetch(`/api/analytics/${userId}`).then(r => r.json()),
]);
const pick = (i, fallback) =>
results[i].status === "fulfilled" ? results[i].value : fallback;
return {
profile: pick(0, null),
notifications: pick(1, []),
billing: pick(2, null),
analytics: pick(3, null),
};
}
كل خدمة فشلت بتدي fallback مفهوم للـ UI، والمستخدم بيكمل شغله. الـ logging للفشل بيروح لـ Sentry أو أي observability tool، مش للمستخدم.
الأرقام: إيه الفرق فعلاً في الأداء
في قياس بسيط على 4 طلبات متوازية، متوسط latency كل واحد 180ms، وواحد منهم بيعمل timeout بعد 3 ثواني: Promise.all بترجع بعد ~180ms لو مفيش فشل، وبترمي خطأ بعد 3 ثواني لو فيه فشل. Promise.allSettled بتستنى الـ 3 ثواني كاملة دايمًا في حالة الفشل. التكلفة: ثواني زيادة في worst case. المكسب: مفيش بيانات بتضيع، ومفيش حاجة اسمها "كل حاجة فشلت" من طلب واحد.
الحل لمشكلة الانتظار: حط timeout صريح على كل fetch بـ AbortController. كده أسوأ حالة = الـ timeout اللي إنت حاطه (مثلًا 2 ثانية)، مش زمن timeout الخدمة البايظة.
function fetchWithTimeout(url, ms = 2000) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), ms);
return fetch(url, { signal: ctrl.signal })
.finally(() => clearTimeout(t));
}
trade-offs لازم تحطها في الحسبة
- زمن الاستجابة:
allSettledبيتحكم فيها أبطأ promise دايمًا. بدون timeout صريح، خدمة واحدة بايظة ممكن تخلي الـ dashboard يفضل loading 30 ثانية. - التعامل مع الأخطاء:
allSettledمش بترفع exception تلقائيًا. لازم تفحصstatusيدويًا، وإلا هتفاجأ بـundefinedفي الـ UI ومفيش log للفشل. - الـ type safety: في TypeScript النوع هو
PromiseSettledResult<T>اللي هو union بينPromiseFulfilledResultوPromiseRejectedResult. كل قراءة لـvalueمحتاجة type guard علىstatus.
متى لا تستخدم Promise.allSettled
لو كل الطلبات شرط لنجاح العملية، فـ Promise.all هي الصح. مثال: ترنزاكشن مالية بتتطلب 3 تأكيدات، فشل أي واحد = ترجع العملية كلها. كمان في parallel writes على قاعدة بيانات، لو مشيت بعد فشل جزئي هتخلق data inconsistency. القاعدة: لو ما تقدرش تكمل بنتيجة ناقصة، استخدم all، مش allSettled.
الافتراضات
الشرح ده مبني على بيئة تدعم ES2020 (Node 12.9+، كل المتصفحات الحديثة من 2020). لو Node عندك أقدم من كده، هتحتاج polyfill أو ترقية. الأرقام في قسم الأداء بتفترض طلبات IO مستقلة مش CPU-bound work، ومفيش rate limiting مفاجئ من أي خدمة.
الخطوة التالية
افتح ملف فيه Promise.all في مشروعك دلوقتي. اسأل سؤال واحد: لو طلب واحد فشل، هل المستخدم محتاج النتيجة الكاملة، ولا كل طلب مستقل؟ لو الإجابة "كل طلب مستقل"، حوّلها لـ allSettled مع timeout على كل fetch. لو "كل أو لا شيء"، سيبها زي ما هي وضيف log واضح للفشل.