ما هو الـ Promise في جافا سكريبت؟ ببساطة هو وعد بقيمة قادمة
عندما تحتاج جافا سكريبت إلى تنفيذ عملية تستغرق وقتًا — كطلب شبكة، أو قراءة ملف، أو انتظار مؤقت — فهي لا تستطيع أن تسلّمك النتيجة فورًا. بدلًا من ذلك، تعطيك كائنًا من نوع Promise: كائن يمثّل قيمة ستوجد لاحقًا.
أول console.log يطبع Promise في حالة pending، وبعد نصف ثانية يتحقق الـ Promise ويتم تنفيذ الـ callback الخاصة بـ .then مع القيمة. الـ Promise في جوهره مجرد كائن عادي، لكن السحر يكمن في قدرته على إخطار كل من ينتظر قيمته فور وصولها.
الحالات الثلاث لأي Promise
أي Promise لا يخرج عن ثلاث حالات:
- pending — العملية لا تزال جارية، ولا توجد قيمة بعد.
- fulfilled — العملية نجحت، وأصبحت القيمة متاحة.
- rejected — العملية فشلت، وظهر خطأ.
ينتقل الـ Promise من pending إلى fulfilled أو rejected مرة واحدة فقط، ويبقى في تلك الحالة إلى الأبد. لا يمكنك التراجع عن تحقّق الـ Promise ولا تحقيقه مرتين.
Promise.resolve(value) تُنشئ Promise جاهزة ومكتملة مسبقًا، بينما Promise.reject(error) تُنشئ Promise مرفوضة من البداية. هاتان الدالتان مفيدتان جدًا في الاختبارات، وأيضًا عندما تريد من دالة أن تُرجع Promise في حالات يكون فيها الجواب متاحًا فورًا.
قراءة القيمة باستخدام .then و .catch
لا يمكنك استخراج القيمة من الـ Promise مباشرة، بل تُمرّر دالة callback إلى .then، وتقوم الـ Promise باستدعائها بنفسها عندما تصبح القيمة جاهزة:
.catch(fn) تنفّذ دالّتها في حال رُفض الـ Promise. تحت الغطاء، هي مجرّد اختصار لـ .then(undefined, fn). ووضع .catch() في نهاية السلسلة يكفي لالتقاط أي رفض يحصل في أي خطوة قبله — فلا داعي لتكرارها بعد كل .then.
تسلسل الـ Promises: كل .then يُرجع Promise جديدًا
هنا بالضبط يقع أغلب المطوّرين في الحيرة. الدالّة .then() لا تكتفي بتشغيل الـ callback، بل تُرجع Promise جديدًا يُحَلّ بالقيمة التي أعادها ذلك الـ callback. وهذا ما يتيح لك بناء سلسلة متّصلة:
كل خطوة تُغذّي التي تليها. ولو أرجعت دالة .then وعداً (Promise)، فإن السلسلة تنتظر اكتمال هذا الوعد قبل أن تُكمل — وبهذا تتراكب الخطوات غير المتزامنة بشكل أنيق:
ثلاث خطوات غير متزامنة متسلسلة، بدون أي تداخل. جرّب تكتب نفس المنطق باستخدام callbacks وستفهم فورًا لماذا انتشرت الـ Promises بهذه السرعة.
الأخطاء تنساب عبر السلسلة
عندما يُرفض الـ Promise، فإنه يتجاوز كل .then حتى يصل إلى أول .catch في طريقه. هذا هو نموذج معالجة الأخطاء بأكمله:
رمي استثناء داخل .then يؤدي إلى رفض الـ Promise الذي أعاده هذا الـ .then. بعد ذلك تمرّ كل .then التالية على الرفض وتُمرّره كما هو، حتى يلتقطه .catch ويبتلعه. في الغالب يكفيك وضع .catch واحد في نهاية السلسلة — أما السلسلة التي لا تحتوي على .catch إطلاقًا فستُطلق تحذير "unhandled promise rejection"، وهو تحذير يستحق أن تعالجه.
إنشاء Promise خاص بك باستخدام new Promise
في أغلب الأحيان ستتعامل مع Promises جاهزة تقدّمها لك المكتبات. لكن أحيانًا تحتاج إلى تغليف شيء لا يُرجِع Promise أصلًا — عادةً تكون واجهة قديمة تعتمد على دوال الـ callback:
الدالة التي تمررها إلى new Promise تُسمّى المنفّذ (executor). تستقبل وسيطين: resolve (استدعِها مع قيمة النجاح) وreject (استدعِها مع الخطأ). استدعِ أحدهما مرة واحدة فقط، وأي استدعاء بعد ذلك سيتم تجاهله.
عادتان ستوفّران عليك الكثير من الصداع:
- لا تلجأ إلى
new Promiseإلا عند تغليف شيء لا يعتمد أصلاً على الـ Promises. أما إذا كانت الدالة تُرجع Promise بالفعل، فاكتفِ بإرجاعها كما هي. - عند استخدام
reject، مرّر دائماً كائنErrorبدلاً من نص عادي، حتى تحتفظ بتتبّع الاستدعاءات (stack trace).
تنفيذ المهام بالتوازي باستخدام Promise.all
سلاسل .then تُنفَّذ بالتتابع واحدة تلو الأخرى. لكن عندما تكون لديك عدة مهام غير متزامنة ومستقلة عن بعضها وتريد تشغيلها في الوقت نفسه، فإن Promise.all هو الأداة المناسبة:
المؤقتات الثلاثة تشتغل بالتوازي. الدالة Promise.all ترجع مصفوفة بالنتائج بنفس ترتيب المدخلات — لكن فقط بعد ما تكتمل كل الـ Promises. الوقت الإجمالي تقريباً 400ms، مش 900ms.
لكن في مصيدة: Promise.all بتفشل (reject) بمجرد ما أي Promise من ضمنها تفشل، وبتضيع باقي النتائج. ده التصرف المناسب لما تكون محتاج كل الأجزاء فعلاً (زي عرض صفحة تعتمد على ثلاثة استدعاءات API). أما لما ما تكونش محتاج ده، استخدم allSettled.
لما يكون مقبول إن بعض الطلبات تفشل: Promise.allSettled
الدالة Promise.allSettled بتستنى كل الـ Promises تخلص — سواء نجحت أو فشلت — وبترجعلك تقرير بالنتيجة:
كل نتيجة عبارة عن كائن بالشكل: { status: "fulfilled", value } أو { status: "rejected", reason }. هذا الأسلوب مفيد في الحالات التي يكون فيها النجاح الجزئي مقبولاً — مثل تسجيل دفعة من الأحداث، أو جلب مجموعة من الصور المصغّرة، أو تشغيل فحوصات صحّة مستقلّة.
وهناك دالتان أخريان من نفس العائلة تستحقّان المعرفة:
Promise.race([...])— تُحسم بمجرّد أن تُحسم أول Promise في القائمة، سواء بالنجاح أو بالفشل. مفيدة جداً لتطبيق مهل زمنية (timeouts).Promise.any([...])— تنجح مع أول Promise ناجحة وتتجاهل حالات الفشل. ولا تفشل إلا إذا فشلت جميع الـ Promises.
الـ Promises غير متزامنة دائماً
حتى لو كانت الـ Promise محسومة مسبقاً، فإن دالة .then الخاصة بها تُستدعى بشكل غير متزامن — لن تعمل بشكل متزامن أبداً، ولن تعمل في نفس الدورة (tick) الحالية:
الناتج هو قبل ثم بعد ثم فوري. الـ callback الخاص بـ .then ينتظر حتى ينتهي الكود الحالي، ثم يُنفَّذ عبر طابور المهام الدقيقة (microtask queue). هذه القاعدة — "لا يعمل أي callback تابع لـ Promise بشكل متزامن أبداً" — هي السبب في أن دمج الـ Promises مع الكود المتزامن يكون سلوكه متوقَّعاً: الكود المتزامن ينتهي أولاً دائماً.
الخطوة التالية: async/await
تسلسل استدعاءات .then يؤدي الغرض، لكن بمجرد أن تتجاوز خطوتين أو ثلاث يبدأ الكود يشبه السلالم. صيغة async/await ما هي إلا طبقة فوق الـ Promises تتيح لك كتابة نفس المنطق وكأنه متزامن — مع استخدام try/catch لمعالجة الأخطاء ومتغيرات عادية للقيم الوسيطة. هذا ما سنتناوله تالياً.
الأسئلة الشائعة
ما هو الـ Promise في جافا سكريبت؟
الـ Promise ببساطة هو كائن يمثّل قيمة لم تصل بعد — غالباً نتيجة عملية غير متزامنة مثل طلب شبكة. يكون دائماً في إحدى ثلاث حالات: pending أو fulfilled أو rejected، وتحصل على القيمة النهائية بربط دوال عبر .then() و .catch().
ما الفرق بين then و catch؟
.then(onFulfilled) تُنفَّذ عندما ينجح الـ Promise وتستقبل القيمة الناتجة، بينما .catch(onRejected) تُنفَّذ عند فشل الـ Promise (أو أي Promise سابق في نفس السلسلة) وتستقبل الخطأ. وضع .catch() واحدة في نهاية السلسلة كافٍ للتقاط أي خطأ يحدث في أي خطوة قبلها.
ماذا يفعل Promise.all؟
Promise.all([p1, p2, p3]) يأخذ مصفوفة من الوعود ويرجّع Promise واحد يُحلّ بمصفوفة تحتوي كل القيم الناتجة — لكن فقط بعد أن تنجح جميع الوعود. وإذا فشل أي واحد منها، يفشل الكل مباشرة. إذا أردت الحصول على كل النتائج بغض النظر عن الأخطاء، استخدم Promise.allSettled بدلاً منه.
أستخدم Promises أم async/await؟
كلاهما نفس الآلية تحت الغطاء — async/await ما هي إلا صياغة مبنية فوق الـ Promises. الكود الجديد يُقرأ بشكل أوضح مع async/await، لكنك ما زلت تُرجِع Promises، وتمسك الأخطاء بـ try/catch أو .catch()، وتستخدم Promise.all لتشغيل المهام بالتوازي. فهم الـ Promises جيداً يجعل async/await منطقياً.