كن مطوّر واجهات أمامية محترفا
6 دقيقة قراءة

Async/Await: شيفرة غير متزامنة تُقرأ كأنها متزامنة

دليل عملي كامل لـ async/await — كيف يبني على الوعود، وكلمة await، ومعالجة الأخطاء بـ try/catch، وتشغيل المهام بالتوازي مقابل التتابع، ودوال async التي تُرجِع وعدًا دائمًا، وtop-level await، والحلقات وعدم التزامن، والأخطاء الشائعة — مع تمارين عملية وحلولها.

async/await هي الصياغة التي جعلت JavaScript غير المتزامنة ممتعة الكتابة. ليست آلية جديدة — بل طبقة رقيقة جميلة فوق الوعود تتيح كتابة شيفرة تُقرأ من الأعلى للأسفل كشيفرة متزامنة عادية، بينما تعمل غير متزامنة تحتها. إن فهمت الوعود، فـ async/await غالبًا عن تعلّم أين تضع كلمتين وكيف تتدفّق الأخطاء. (إن كانت الوعود غير راسخة، اقرأ ذلك المقال أولًا — كل شيء هنا مبنيّ عليها.)

await يوقف دالّة async حتى يستقرّ وعد، ثم يستأنف بقيمته — دون حجب بقية الصفحة. ودالّة async تُرجِع وعدًا دائمًا، مهما أرجعت داخلها. هاتان الحقيقتان تفسّران كل السلوك.

من سلاسل .then إلى await

المنطق نفسه، مكتوبًا بالطريقتين. await يفكّ قيمة الوعد إلى متغيّر، فتصير السلسلة تتابعًا بسيطًا من العبارات:

// سلسلة وعود
function loadUser() {
  return fetch("/api/user")
    .then(res => res.json())
    .then(user => fetch(`/api/posts?id=${user.id}`))
    .then(res => res.json());
}

// الشيء نفسه بـ async/await
async function loadUser() {
  const res = await fetch("/api/user");
  const user = await res.json();
  const postsRes = await fetch(`/api/posts?id=${user.id}`);
  return postsRes.json();
}

كل await يقول "توقّف هنا حتى يستقرّ هذا الوعد، ثم أعطِني قيمته وتابِع." تُعلَّق الدالّة دون تجميد المتصفح — تبقى شيفرة أخرى ونقرات ورسم تعمل أثناء انتظارها.

قاعدتان تفسّران كل شيء

1. await يعمل فقط داخل دالّة async (أو في المستوى الأعلى لوحدة). لا يمكنك رشّه في دالّة عادية:

async function go() {
  const data = await fetch("/api/data");  // ✅
}
function bad() {
  const data = await fetch("/api/data");  // ❌ خطأ صياغة
}

2. دالّة async تُرجِع وعدًا دائمًا. إرجاع قيمة عادية يلفّها في وعد مُنجَز؛ والرمي يرفضه:

async function getNumber() {
  return 42;            // يتلقّى المستدعي وعدًا يُنجَز بـ 42
}
getNumber().then(n => console.log(n)); // 42

async function fail() {
  throw new Error("لا"); // يتلقّى المستدعي وعدًا مرفوضًا
}
fail().catch(e => console.log(e.message)); // "لا"

لهذا ما زلت تحتاج await (أو .then) لـ استخدام نتيجة دالّة async — استدعاؤها يسلّمك وعدًا فقط.

معالجة الأخطاء بـ try/catch

المكسب المريح الأكبر: تُلتقَط الأخطاء غير المتزامنة بنفس try/catch الذي تستخدمه للشيفرة المتزامنة. وعد مرفوض تنتظره بـ await يرمي، فتعالج كتلة واحدة كليهما:

async function loadUser(id) {
  try {
    const res = await fetch(`/api/user/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`); // فحصك الخاص
    return await res.json();                            // خطأ التحليل يحطّ هنا أيضًا
  } catch (err) {
    console.error("فشل تحميل المستخدم:", err);
    throw err;          // أعد رميه إن احتاج المستدعي أن يعرف
  } finally {
    hideSpinner();      // يعمل دائمًا
  }
}

كتلة catch تلتقط فشل الشبكة، ورميك اليدويّ، وخطأ تحليل JSON — كل فشل في try يتجمّع في مكان واحد، تمامًا كالشيفرة المتزامنة.

التوازي مقابل التتابع — فخّ الأداء

هذا هو الخطأ الذي يبطّئ التطبيقات الحقيقية بهدوء. كل await يوقف حتى يستقرّ وعده، فانتظار شيئين مستقلّين على سطرين منفصلين يشغّلهما واحدًا تلو الآخر:

// ❌ تتابعي — getB لا يبدأ حتى تنتهي getA
const a = await getA();
const b = await getB();   // الوقت الكلّي ≈ A + B

إن لم يعتمد أحدهما على الآخر، ابدأ كليهما أولًا، ثم انتظر معًا بـ Promise.all:

// ✅ متوازٍ — كلاهما يبدأ فورًا
const [a, b] = await Promise.all([getA(), getB()]); // الوقت الكلّي ≈ max(A, B)

القاعدة: await على سطور منفصلة فقط حين تحتاج كل خطوة فعلًا نتيجة السابقة. وإلا، أطلِقها بالتوازي.

الحلقات وعدم التزامن

await داخل حلقة for...of يشغّل التكرارات بالتتابع — كلٌّ ينتظر السابق. أحيانًا هذا ما تريد (حدود المعدّل، كتابات مرتّبة)؛ وغالبًا ليس كذلك:

// تتابعي — واحد في كل مرّة (أحيانًا مقصود)
for (const id of ids) {
  await processOne(id);
}

// متوازٍ — كلها دفعة، ثم انتظر المجموعة
await Promise.all(ids.map(id => processOne(id)));

احذر: forEach لا ينتظر — arr.forEach(async ...) يُطلِق كل ردود النداء لكن الدالّة المحيطة لن تنتظرها. استخدم for...of (تتابعي) أو Promise.all(map(...)) (متوازٍ) بدلًا منه.

Top-Level Await

في الوحدات، تستطيع await في المستوى الأعلى دون لفّه في دالّة async — مفيد للتهيئة:

// داخل وحدة (.mjs أو type="module")
const config = await fetch("/config.json").then(r => r.json());
export const apiUrl = config.apiUrl;

هذا يعمل فقط في الوحدات، ويؤخّر تقييم الوحدة حتى يستقرّ الوعد — جيّد للإعداد، لكن لا تحجب على شيء بطيء كان يمكن لبقية التطبيق المضيّ بدونه.

خلط await وتوابع الوعود

async/await والوعود هما الشيء نفسه — اخلطهما بحرّية. انتظر مجمّعًا بـ await، أو استدعِ .catch على نتيجة دالّة async:

const results = await Promise.allSettled(urls.map(u => fetch(u)));

// دالّة async تُرجِع وعدًا، فهذا صحيح:
loadUser(1).catch(err => showError(err));

الجأ إلى await للوضوح، وانزل إلى Promise.all/race/allSettled حين تنسّق عدّة وعود — تتركّب بسلاسة.

الأخطاء الشائعة

  • نسيان await — تحصل على وعد معلّق بدل القيمة، وif (promise) صادق دائمًا.
  • انتظارات تتابعية لعمل مستقلّ — خطأ الأداء الكلاسيكي؛ استخدم Promise.all.
  • array.forEach(async …) متوقّعًا أن ينتظر — لا يفعل؛ استخدم for...of أو Promise.all(map(...)).
  • بلا try/catch حول await — رفض غير مُعالَج يُعطّل أو يحذّر بصوت عالٍ.
  • نسيان أن دالّة async تُرجِع وعدًا، ثم استخدام قيمتها المُرجَعة كأنها النتيجة الخام.
  • الإفراط في try/catch حول كل سطر بدل كتلة واحدة لكل عملية منطقية.
  • الحجب بـ top-level await على شيء بطيء لم يكن يحتاج لتعطيل بدء التشغيل.

تمارين

جرّب كلًّا منها قبل فتح الحل.

تمرين 1 — أعد كتابة سلسلة

حوّل هذا إلى async/await:

function getTitle() {
  return fetch("/api/page").then(r => r.json()).then(p => p.title);
}
إظهار الحل
async function getTitle() {
  const res = await fetch("/api/page");
  const page = await res.json();
  return page.title;
}

كل .then يصير سطر await. ما زالت الدالّة تُرجِع وعدًا (لأنها async)، فيستدعيها المستدعون بـ await getTitle().

تمرين 2 — اكتشف الشيفرة البطيئة

const user = await getUser();
const posts = await getPosts();
const tags = await getTags();

الثلاثة مستقلّة. اجعلها سريعة.

إظهار الحل
const [user, posts, tags] = await Promise.all([getUser(), getPosts(), getTags()]);

الأصل يشغّلها واحدة تلو الأخرى (مجموع الثلاثة). وبما أن لا واحدة تعتمد على أخرى، يشغّلها Promise.all معًا — الوقت الكلّي هو الأبطأ فقط.

تمرين 3 — مساعد جلب متين

اكتب getJSON(url) غير متزامن يرمي خطأً واضحًا عند استجابة غير OK، ويُرجِع الجسم المحلَّل خلاف ذلك.

إظهار الحل
async function getJSON(url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`فشل الطلب إلى ${url}: ${res.status}`);
  return res.json();
}

fetch يرفض فقط عند فشل الشبكة، لا عند 404/500 — فعليك فحص res.ok بنفسك والرمي، تاركًا try/catch المستدعي يعالجه.

تمرين 4 — حلقة تتابعية مقابل متوازية

لديك ids وsave(id) غير متزامن. اكتب كليهما: واحد يحفظ بالترتيب الصارم، وآخر يحفظ الكلّ بالتزامن.

إظهار الحل
// بالترتيب — كلٌّ ينتظر السابق
for (const id of ids) {
  await save(id);
}

// بالتزامن — كلها دفعة
await Promise.all(ids.map(id => save(id)));

for...of مع await يسلسل؛ والتحويل إلى وعود وPromise.all يوازي. اختر حسب أهمّية الترتيب/المعدّل.

النموذج الذهني الذي تحتفظ به

async/await هي الوعود بثياب أجمل. await يوقف دالّة async حتى يستقرّ وعد ويسلّمك قيمته — دون حجب الصفحة — ودالّة async تُرجِع وعدًا دائمًا، فما زلت تنتظر (أو .then) نتيجتها. تصير الأخطاء رميًا عاديًّا، فلُفّ المكالمات المنتظَرة في try/catch فتعالج الإخفاقات المتزامنة وغير المتزامنة معًا. والعادة الوحيدة التي تفصل السريع عن البطيء: استخدم await تتابعيًّا فقط حين تحتاج خطوة نتيجة السابقة، والجأ إلى Promise.all لحظة استقلال العمليات. تحتها كله ما زال آلية الوعود — مهام دقيقة، وحالات، واستقرار — معبَّرًا عنه فقط كشيفرة تقرؤها مباشرةً نزولًا في الصفحة.