Menu
العربية

async/await في JavaScript: شرح شامل مع أمثلة

تعلّم كيف تعمل async/await في جافا سكريبت فعلياً: الدوال غير المتزامنة، انتظار الـ promises، التعامل مع الأخطاء عبر try/catch، وتنفيذ المهام بالتوازي.

async/await ما هي إلا وعود (Promises) بحلّة جديدة

async/await ليست نموذجًا جديدًا للتزامن في جافا سكريبت، بل هي مجرد صياغة ألطف فوق الـ Promises تتيح لك كتابة كود يبدو تسلسليًا رغم أنه غير متزامن. نفس الآلية تحت الغطاء، لكن بشكل أكثر وضوحًا وسهولة.

إليك نفس المهمة مكتوبة بالطريقتين:

index.js
Output
Click Run to see the output here.

كلتا الدالتين تُرجعان وعداً (Promise)، وكلتاهما تؤديان نفس المهمة تماماً. لكن النسخة التي تستخدم async تُقرأ من الأعلى إلى الأسفل دون الحاجة إلى سلسلة .then — وهذه هي نقطة الجاذبية الأساسية.

async تجعل الدالة تُرجع Promise

بمجرد أن تضع الكلمة async قبل function عادية أو دالة سهمية أو دالة تابعة لكائن (method)، يحدث أمران:

  1. الدالة ستُرجع وعداً دائماً. وأي قيمة تُعيدها بـ return تصبح هي القيمة التي يتحقق بها الوعد (resolved value).
  2. يُصبح بإمكانك استخدام await داخل هذه الدالة.
index.js
Output
Click Run to see the output here.

لاحظ أن result ليس النص نفسه، بل هو وعد (Promise) يُحَلّ إلى النص. فحتى لو لم تحتوِ الدالة greet على أي await ولا أي عملية غير متزامنة، فإن كلمة async وحدها كفيلة بتغليف القيمة المُرجَعة داخل Promise. وإذا رُمي استثناء داخل الدالة، فإن الوعد يُرفَض.

كيف يوقف await التنفيذ حتى ينتهي الـ Promise

داخل أي دالة async، فإن التعبير await somePromise يوقف تنفيذ هذه الدالة مؤقتًا إلى أن يُحَلّ الوعد، ثم يُعيد لك القيمة الناتجة عنه. أما إذا رُفِض الوعد، فإن await يرمي استثناءً.

index.js
Output
Click Run to see the output here.

انتبه لترتيب المخرجات. ستجد أن "بدأ العد التنازلي" يُطبع قبل "٢" — والسبب أن await يوقف الدالة async فقط، لا بقية البرنامج. حلقة الأحداث (event loop) تواصل عملها، ودالة countdown تستأنف لاحقًا كلما تحقّق وعد wait.

يمكنك استخدام await مع أي قيمة شبيهة بالوعود. حتى await 42 صحيح تمامًا — فالقيم التي ليست وعودًا تُغلَّف تلقائيًا داخل Promise.resolve(42) ويُحلّ الوعد فورًا.

معالجة الأخطاء باستخدام try/catch مع async await

مع الوعود التقليدية كنا نسلسل .catch()، أما مع async/await فإن الوعد المرفوض يتحول إلى استثناء عادي يمكنك التقاطه بالطريقة المعتادة:

index.js
Output
Click Run to see the output here.

كتلة try/catch واحدة كافية لتغطية كل await بداخلها. أخطاء الشبكة، وفشل تحليل JSON، وحتى عبارات throw التي تكتبها بنفسك، كلها تنتهي في نفس catch. هذه قفزة حقيقية مقارنةً بسلاسل .then/.catch المتشعبة.

لكن انتبه لنقطة مهمة: fetch لا يرفض الوعد إلا عند حدوث خطأ شبكة، أما استجابات HTTP من نوع 4xx و5xx فلا يعتبرها فشلاً. لذلك عليك فحص res.ok يدوياً ورمي الخطأ بنفسك — نمط ستراه بشكل متكرر جداً في الشيفرة الواقعية.

تجنّب استخدام await داخل الحلقات دون داعٍ

هذا أشهر خطأ يقع فيه المطورون عند التعامل مع async/await. استخدام await بشكل متتابع داخل حلقة يعني أن كل دورة ستنتظر انتهاء الدورة التي قبلها:

index.js
Output
Click Run to see the output here.

sequential تستغرق حوالي 900 مللي ثانية، بينما parallel لا تتجاوز 300 مللي ثانية. القاعدة بسيطة: إذا كانت المهام لا تعتمد نتائج بعضها على بعض، أطلقها كلها دفعة واحدة ثم استخدم await Promise.all. أما await واحدة تلو الأخرى فلا معنى لها إلا حين يحتاج الاستدعاء التالي فعلاً إلى نتيجة سابقه.

عند التعامل مع المصفوفات، الأسلوب المعتاد هو Promise.all(items.map(async (x) => ...)). أما حلقة for...of مع await بداخلها فتنفّذ المهام بالتسلسل — وأحياناً يكون هذا هو المطلوب (للتحكم في معدل الطلبات أو الحفاظ على الترتيب)، لكنه في الغالب ليس ما تريده.

الدمج بين async/await ووعود Promise العادية

لستَ مضطراً للاختيار بين الأسلوبين. الدوال غير المتزامنة في جافا سكريبت تُرجع وعوداً أصلاً، وawait يعمل مع أي Promise — لذا يمكنك المزج بينهما بحرية:

index.js
Output
Click Run to see the output here.

الأسلوبان قابلان للتبادل فيما بينهما. استخدم await حين تريد كودًا يُقرأ بسلاسة من الأعلى إلى الأسفل، واستخدم .then إذا كنت تحتاج استدعاءً سريعًا لمرة واحدة، أو إذا كنت تعمل خارج سياق دالة async.

استخدام await في المستوى الأعلى (داخل وحدات ES)

في الماضي، كان لا بد من تغليف await داخل دالة async، لأن استخدامه في المستوى الأعلى من السكربت كان ممنوعًا. لكن الأمور تغيّرت: فداخل وحدة ES module (أي ملف بامتداد .mjs أو وسم <script type="module">)، صار بإمكانك كتابة await مباشرةً في المستوى الأعلى:

// في وحدة ES
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const user = await res.json();
console.log(user.name);

يتسبّب استخدام await على المستوى الأعلى (top level await) في تأخير اكتمال الوحدة (module) حتى تُحسم الـ promise التي ننتظرها، وأي وحدة أخرى تستورد هذه الوحدة ستنتظر هي الأخرى. هذا مفيد عند تحميل الإعدادات أو الاستيراد الديناميكي، لكن لا تُفرط فيه: فإنّ await بطيئاً على المستوى الأعلى يُعطّل كل من يستورد الوحدة.

أمّا في ملفات CommonJS أو السكربتات العادية المضمّنة مباشرةً، فإنّ هذا الأسلوب ما زال يُسبّب خطأً من نوع SyntaxError. الحل التقليدي المعروف هو استخدام دالة غير متزامنة تُستدعى فوراً (IIFE):

index.js
Output
Click Run to see the output here.

أخطاء شائعة يقع فيها الجميع مع async await

جولة سريعة على المطبّات التي يتعثّر بها معظم المطوّرين:

  • نسيان كلمة async. استخدام await داخل دالة عادية يسبّب خطأ نحوي (syntax error). الحل هو إضافة async إلى الدالة، أو استدعاء الدالة غير المتزامنة مع .then.
  • نسيان await قبل النتيجة. كتابة const data = getJSON(url); تُعيد لك وعدًا (promise) وليس البيانات نفسها. وإذا تعاملت معه كأنه القيمة المطلوبة، ستجد [object Promise] يظهر في المخرجات.
  • أخطاء غير مُعالَجة (unhandled rejections). إذا استدعيت دالة غير متزامنة وتركتها تعمل دون متابعة (doWork();)، فإن أي خطأ بداخلها سيُبتلع بصمت ما لم تُضِف .catch أو تستخدم await معها داخل try/catch.
  • استخدام forEach مع دوال غير متزامنة. كتابة array.forEach(async (x) => await something(x)) لن تنتظر أي شيء، لأن forEach يتجاهل الـ promises التي تُعيدها الدالة. البديل الصحيح هو استخدام for...of مع await، أو Promise.all(array.map(...)).
index.js
Output
Click Run to see the output here.

شغّل الكود — ستجد أن "انتهى؟" يُطبع قبل أي "تم"، لأن broken تعود دون أن تنتظر شيئًا. أما fixed فتنتظر كل شيء حتى النهاية، ثم تطبع "انتهى!" في الأخير.

متى تستخدم async/await؟

اجعل async/await خيارك الافتراضي في أي كود يقوم بأكثر من خطوة غير متزامنة بالتتابع، أو يحتاج إلى معالجة الأخطاء بأسلوب try/catch. أما الـ promises الخام فاحتفظ بها للحالات البسيطة من سطر واحد، أو لكود المكتبات الذي يعيد promise دون أن ينتظر شيئًا بنفسه، أو حين تحتاج فعلًا إلى أدوات مثل Promise.race أو .finally() ضمن سلسلة.

حين تستخدمه بشكل صحيح، يجعل async/await الكود غير المتزامن يُقرأ كوصفة طبخ: افعل هذا، ثم هذا، ثم هذا. حلقة الأحداث (event loop) لا تزال تعمل كما هي — لكنك تتوقف عن التفكير بعقلية الـ callbacks.

التالي: واجهة fetch

معظم الأمثلة هنا استخدمت fetch كبديل عن "أي عملية غير متزامنة". وهي تستحق وقفة مستقلة — كيف تعمل الطلبات والاستجابات، والتعامل مع JSON، وضبط الـ headers، ولماذا لا يرفض fetch عند أخطاء HTTP. هذا موضوع الصفحة التالية.

الأسئلة الشائعة

ما وظيفة async/await في جافا سكريبت؟

async/await هي صياغة مبنيّة فوق الـ promises تتيح لك كتابة كود غير متزامن وكأنه متزامن. كلمة async تجعل الدالة تُرجع promise تلقائياً، وawait توقف التنفيذ مؤقتاً داخل تلك الدالة حتى يستقر الـ promise ثم تعطيك القيمة الناتجة. في العمق لا شيء تغيّر — لا تزال promises، لكن الكود صار أسهل قراءةً بكثير.

هل يمكنني استخدام await خارج دالة async؟

في المستوى الأعلى من وحدة ES Module نعم، وهذا ما يُعرف بـ top-level await. أما داخل الدوال العادية أو في ملفات CommonJS فلا — ستحصل على خطأ في الصياغة لأن await غير مسموح بها خارج دالة async. الحل المعتاد هو تغليف الكود داخل دالة async ثم استدعاؤها، أو تحويل الملف إلى ES Module.

كيف أتعامل مع الأخطاء عند استخدام async/await؟

غلّف استدعاءات await داخل كتلة try/catch. أي promise يُرفض وتنتظره بـ await سيتحول إلى استثناء (exception) يمكن لكتلة catch التقاطه. أما المهام التي تشغّلها في الخلفية دون await، فأضف لها .catch() على الـ promise المُرجَع حتى لا تنتهي الرفضات كـ unhandled rejection.

هل await يوقف تنفيذ البرنامج كلّه؟

لا. await توقف فقط الدالة async الحالية عند ذلك السطر. الـ event loop يستمر في العمل بشكل طبيعي — المؤقتات تعمل، المهام غير المتزامنة الأخرى تتقدم، وواجهة المستخدم تبقى متجاوبة. الكود المُستدعِي يستلم promise في حالة pending ويكمل عمله فوراً.

تعلّم البرمجة مع Coddy

ابدأ الآن