async/await تجعل كود Promises يبدو قريبًا من التسلسل العادي، لكنها لا تجعل العملية synchronous ولا تنشئ thread جديدة. await توقف تنفيذ ال ـasync function الحالية حتى تستقر Promise وتسمح لبقية event loop بالاستمرار. فهم هذه النقطة يوضح لماذا طلبان مستقلان قد يصبحان بطيئين إذا انتظرتهما بالتتابع، ولماذا try/catch تعمل مع await فقط عندما تنتظر Promise نفسها.
async function ترجع Promise دائمًا
حتى إذا أعدت رقمًا مباشرة، caller تستلم Promise تحل بهذه القيمة. throw داخل async function تحول إلى rejected Promise. لذلك API الخاصة بالدالة asynchronous حتى لو branch معينة لا تنتظر شيئًا.
التسلسل مقابل التوازي
إذا كان request B يعتمد على نتيجة A، await A ثم B صحيحة. إذا كانا مستقلين، ابدأ الاثنين ثم استخدم Promise.all لتقليل الزمن الكلي. لكن التوازي غير المحدود قد يضغط API أو الموارد.
خطوات عملية
- حدد dependencies.
- ابدأ المستقل معًا.
- انتظر Promise.all.
- استخدم concurrency limit للقوائم الكبيرة.
- تعامل مع failure policy بوضوح.
try/catch تحتاج await داخل النطاق
إذا استدعيت function async بدون await ثم خرجت من try، rejection قد تحدث لاحقًا خارج catch. إما await داخل try أو return Promise إلى caller ليعالجها.
Promise.all تفشل سريعًا
إذا رفضت Promise واحدة، Promise.all ترفض، لكن العمليات الأخرى لا تُلغى تلقائيًا. إذا تحتاج جمع كل النتائج استخدم allSettled، وإذا تحتاج cancellation استخدم AbortController أو دعم المكتبة.
Timeout ليست مدمجة في await
await fetch قد تنتظر حسب سلوك الشبكة والمكتبة. استخدم AbortController/timeout policy. وعند retry، تأكد أن العملية idempotent أو لها key حتى لا تكرر side effect.
لا تستخدم forEach مع async كما تتوقع
forEach لا تنتظر callback. استخدم for...of للتسلسل، أو map إلى Promises ثم Promise.all للتوازي. اختيارك يجب أن يكون مقصودًا.
قائمة مراجعة
- Dependencies واضحة.
- التوازي محدود.
- Errors تصل لل caller.
- Timeout موجودة.
- Retry آمنة.
- لا unhandled rejections.
مثال ذهني بسيط
فكر في await كعلامة تقول: «عندما تنتهي هذه Promise، أكمل هذه function من هنا». هي لا توقف البرنامج كله. لذلك ترتيب السطور داخل function لا يساوي حجز thread، لكنه يحدد dependencies التي فرضتها أنت.
الإلغاء يحتاج AbortController أو API تدعمه
إذا أغلق المستخدم الشاشة أو تغير search query، لا تترك request القديمة تعمل بلا حاجة. مرر AbortSignal إلى fetch أو library تدعمها، وميز AbortError عن failure حقيقية حتى لا تعرض رسالة خطأ للمستخدم على عملية ألغاها بنفسه.
ضع حدًا للتوازي في القوائم
Promise.all على عشرة عناصر مناسب، لكن على عشرة آلاف قد يفتح اتصالات كثيرة ويستهلك memory. استخدم worker pool أو limiter بعدد متزامن معروف. التوازي bounded يحافظ على throughput بدون ضرب rate limits أو الموارد.
مرر الأخطاء بدل ابتلاعها
catch فارغة تجعل caller يعتقد أن العملية نجحت. إذا عالجت الخطأ محليًا فأعد قيمة حالة واضحة، وإلا أعد throw مع context لا يحتوي secrets. في services متعددة الطبقات، أضف error code ثابتة بدل تغيير message نصية يعتمد عليها caller.
شارك في تقييم ونقاش المقال
رأيك يضيف قيمة للمقال ويساعدنا على تحسين المحتوى والنقاش حوله.
النقاش
جارٍ تحميل التعليقات…