Promise.allSettled بالعربي: خلّي الصفحة تكمل رغم فشل API
لو صفحة المنتج عندك بتكسر كلها عشان API واحد وقع، المقال ده هيخليك تعرض البيانات المتاحة وتفصل الفشل بدل ما تعاقب المستخدم على خدمة واحدة.
مستوى القارئ: متوسط
المشكلة باختصار
افترض إن صفحة منتج بتجيب السعر، التقييمات، والمخزون من 3 endpoints مختلفة. السعر بيرجع في 120ms، المخزون في 160ms، لكن reviews API بيعمل timeout بعد 900ms. مع Promise.all، فشل reviews بيرفض العملية كلها. النتيجة: صفحة فاضية أو error عام رغم إن عندك بيانات كفاية تعرضها.
الطريقة دي بتفشل في الواجهات اللي تقبل partial data. بدل ما توقف الصفحة كلها، الأفضل تسأل كل promise: نجحت ولا فشلت؟ هنا يظهر دور Promise.allSettled.
الفكرة بمثال واضح
ركز في المثال ده. عندك فريق بيجهز طلب: شخص جاب السعر، شخص جاب حالة المخزون، وشخص فشل يجيب التقييمات. هل تلغي الطلب كله؟ غالبًا لا. تعرض السعر والمخزون، وتكتب مكان التقييمات: غير متاحة مؤقتًا.
بالظبط ده اللي بيعمله Promise.allSettled. هو لا يرمي exception لمجرد إن promise واحدة فشلت. هو يرجع array فيها نتيجة كل promise: fulfilled ومعاها value، أو rejected ومعاها reason.
مثال JavaScript قابل للنسخ
الافتراض إن عندك واجهة متجر، وكل endpoint مستقل. لو التقييمات فشلت، الصفحة تفضل قابلة للاستخدام.
async function loadProductPage(productId) {
const requests = [
fetch(`/api/products/${productId}/price`).then(r => r.json()),
fetch(`/api/products/${productId}/reviews`).then(r => r.json()),
fetch(`/api/products/${productId}/stock`).then(r => r.json()),
];
const [priceResult, reviewsResult, stockResult] = await Promise.allSettled(requests);
return {
price: priceResult.status === "fulfilled" ? priceResult.value : null,
reviews: reviewsResult.status === "fulfilled" ? reviewsResult.value : [],
stock: stockResult.status === "fulfilled" ? stockResult.value : { available: false },
warnings: [priceResult, reviewsResult, stockResult]
.map((result, index) => ({ result, index }))
.filter(item => item.result.status === "rejected")
.map(item => `request_${item.index}_failed`),
};
}في سيناريو واقعي، صفحة فيها 3 أقسام. مع Promise.all وفشل request واحد، المعروض يساوي 0 أقسام. مع Promise.allSettled، ممكن تعرض 2 من 3 أقسام. الرقم هنا بسيط لكنه مهم: بدل 0% من الصفحة، تعرض حوالي 66% من المحتوى المفيد.
إمتى تستخدم Promise.all وإمتى allSettled
استخدم Promise.all لما كل النتائج تعتمد على بعض. مثال: لازم تجيب user session، بعدها permissions، بعدها invoice. لو خطوة فشلت، الباقي ملوش معنى.
استخدم Promise.allSettled لما النتائج مستقلة. مثال: dashboard فيه revenue، alerts، وlatest comments. فشل comments لا يمنع عرض revenue. الـ trade-off هنا واضح: بتكسب واجهة أكثر تحملًا للفشل، لكن بتخسر بساطة الكود. لازم تعمل handling لكل حالة بدل catch واحد عام.
كمان خليك واعي إن allSettled لا يوقف الطلبات البطيئة. هو ينتظر كل promises تنتهي، سواء نجاح أو فشل. لو عندك timeout مهم، استخدم AbortController أو timeout wrapper بجانبها.
قياس عملي قبل وبعد
لو عندك 50K زيارة يوميًا وصفحة المنتج تعتمد على 3 APIs، ومعدل فشل reviews API هو 2%، فأنت ممكن تكسر حوالي 1000 مشاهدة يوميًا بدون داعي. بعد التحويل إلى allSettled، نفس الـ 1000 مشاهدة تقدر تعرض السعر والمخزون مع رسالة fallback للتقييمات.
المكسب: تقليل صفحات الخطأ الظاهرة للمستخدم. الخسارة: محتاج تصميم fallback واضح، logging جيد، وتنبيه لما failure rate يزيد. لو تجاهلت المراقبة، ممكن تخفي مشكلة حقيقية تحت واجهة تبدو شغالة.
متى لا تستخدم هذه الطريقة
لا تستخدم Promise.allSettled لو فشل جزء واحد يجعل النتيجة كلها غير آمنة. صفحة دفع لا ينفع تعرضها جزئيًا لو فشل حساب الضرائب أو تحقق المخزون النهائي. في الحالات دي، الفشل الصريح أفضل من قرار ناقص.
ولا تستخدمها كبديل عن retry أو circuit breaker. هي طريقة لتجميع النتائج، مش علاج لتعطل الخدمة نفسها.
مصادر اعتمدت عليها
- MDN: توثيق
Promise.allSettled()يوضح شكل النتائج وحالاتfulfilledوrejected. - MDN: توثيق
Promise.all()يوضح سلوك الرفض السريع عند فشل أي promise. - MDN: توثيق
AbortControllerمفيد لو عايز تضيف timeout أو إلغاء للطلبات البطيئة.
الخطوة التالية
افتح أول صفحة عندك بتعمل 3 طلبات مستقلة، واستبدل Promise.all بـ Promise.allSettled في تجربة صغيرة. لو المستخدم يقدر يستفيد من نتيجتين رغم فشل الثالثة، يبقى التغيير يستحق الدخول في production بعد إضافة logging.