جاوااسکریپتمتوسط

درک async/await در جاوااسکریپت

راهنمایی کاربردی دربارهٔ پرامیس‌ها و async/await: چطور کار می‌کنند، خطاها را چطور مدیریت کنیم و کارها را چطور موازی اجرا کنیم تا برنامه کند نشود.

خط زمانی که اجرای پشت‌سرهمِ سه درخواست را با اجرای موازیِ همان درخواست‌ها مقایسه می‌کند
در این صفحه

جاوااسکریپت کد شما را روی یک رشته (thread) اجرا می‌کند، با این حال می‌تواند فایل دانلود کند، منتظر تایمرها بماند و با پایگاه داده حرف بزند بی‌آنکه قفل شود. راز این کار کد ناهمگام (asynchronous) است و async/await خواناترین راه برای نوشتن آن است.

در این مقاله یاد می‌گیرید پرامیس واقعاً چیست، async/await چطور روی آن بنا شده، خطاها را چطور درست مدیریت کنید و چطور از رایج‌ترین اشتباهِ عملکردیِ برنامه‌نویسان دوری کنید.

مسئله: منتظر ماندن بدون مسدود کردن#

فرض کنید می‌خواهید اطلاعات یک کاربر را از یک API بگیرید. شبکه ممکن است ۳۰۰ میلی‌ثانیه طول بکشد. اگر جاوااسکریپت همان‌جا می‌ایستاد و صبر می‌کرد، کل صفحه قفل می‌شد: نه کلیکی، نه اسکرولی، نه انیمیشنی.

در عوض، توابع ناهمگام بلافاصله یک پرامیس (Promise) برمی‌گردانند؛ شیئی که نمایندهٔ مقداری است که بعداً در دسترس خواهد بود.

پرامیس در ۶۰ ثانیه#

هر پرامیس همیشه در یکی از سه حالت است:

  • pending (در انتظار): کار هنوز در جریان است
  • fulfilled (انجام‌شده): کار با موفقیت و با یک مقدار تمام شده
  • rejected (ردشده): کار با یک خطا شکست خورده

با .then() و .catch() به پرامیس واکنش نشان می‌دهید:

JavaScript
fetch("https://api.github.com/users/octocat")
  .then((response) => response.json())
  .then((user) => console.log(user.name))
  .catch((error) => console.error("Request failed:", error));

این روش کار می‌کند، اما با بزرگ‌تر شدن منطق، دنبال کردن زنجیره‌ها سخت می‌شود؛ مخصوصاً وقتی هر مرحله به نتیجهٔ مرحلهٔ قبل وابسته است.

ورود async/await#

async/await سینتکسی است که روی پرامیس‌ها ساخته شده. با آن می‌توانید کد ناهمگامی بنویسید که مثل کد همگام خوانده می‌شود:

JavaScript
async function showUser(username) {
  const response = await fetch(`https://api.github.com/users/${username}`);
  const user = await response.json();
  console.log(user.name);
}

showUser("octocat");

دو قانون این کار را ممکن می‌کند:

  1. async قبل از تابع باعث می‌شود تابع همیشه یک پرامیس برگرداند.
  2. await همان تابع را متوقف می‌کند تا پرامیس به نتیجه برسد و بعد مقدارش را به شما می‌دهد. در این فاصله بقیهٔ کدها اجرا می‌شوند؛ فقط همین تابع منتظر می‌ماند.

await صفحه را قفل نمی‌کند

وقتی showUser روی یک await متوقف است، مرورگر آزاد است کلیک‌ها را پردازش کند، توابع دیگر را اجرا کند و صفحه را دوباره رسم کند. تابع پس از به نتیجه رسیدن پرامیس ادامه پیدا می‌کند.

مدیریت خطا با try/catch#

در زنجیره‌های .then() از .catch() استفاده می‌کنید. با async/await از همان try/catch آشنا استفاده می‌کنید:

JavaScript
async function getUser(username) {
  try {
    const response = await fetch(`https://api.github.com/users/${username}`);

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    console.error(`Could not load ${username}:`, error.message);
    return null;
  }
}

fetch فقط در خطای شبکه رد می‌شود

پاسخ 404 یا 500 پرامیسِ fetch() را رد نمی‌کند. همیشه خودتان response.ok (یا response.status) را بررسی کنید، همان‌طور که بالا دیدید.

اشتباه شمارهٔ ۱: کدی که ناخواسته پشت‌سرهم اجرا می‌شود#

به این تابع نگاه کنید. مشکلش را می‌بینید؟

JavaScript
async function loadDashboard() {
  const user = await getUser("octocat");     // ~300 ms
  const repos = await getRepos("octocat");   // ~300 ms
  const events = await getEvents("octocat"); // ~300 ms
  return { user, repos, events };            // total: ~900 ms
}

هر درخواست منتظر می‌ماند تا قبلی تمام شود، در حالی که هیچ‌کدام به دیگری وابسته نیستند! همه را هم‌زمان شروع کنید و با Promise.all() با هم منتظرشان بمانید:

JavaScript
async function loadDashboard() {
  const [user, repos, events] = await Promise.all([
    getUser("octocat"),
    getRepos("octocat"),
    getEvents("octocat"),
  ]);
  return { user, repos, events };            // total: ~300 ms
}

همان نتیجه، تقریباً سه برابر سریع‌تر.

⁧ترتیبی⁩await, await, await user repos events ≈ 900 ms ⁧موازی⁩Promise.all() user repos events ≈ 300 ms ⁧۳ برابر سریع‌تر⁩ 0300 ms600 ms900 ms
منتظر ماندنِ تک‌تکِ درخواست‌های مستقل زمان‌هایشان را جمع می‌کند؛ Promise.all آن‌ها را هم‌زمان اجرا می‌کند

انتخاب ترکیب‌کنندهٔ مناسب پرامیس#

متد انجام می‌شود وقتی… رد می‌شود وقتی…
Promise.all() همهٔ پرامیس‌ها انجام شوند هر کدام رد شود
Promise.allSettled() همهٔ پرامیس‌ها به نتیجه برسند هرگز
Promise.race() اولین پرامیسی که به نتیجه می‌رسد انجام شود اولین پرامیسی که به نتیجه می‌رسد رد شود
Promise.any() اولین پرامیس انجام شود همهٔ پرامیس‌ها رد شوند

وقتی همهٔ نتیجه‌ها را می‌خواهید حتی اگر بعضی شکست بخورند، از Promise.allSettled() استفاده کنید؛ مثلاً برای بارگذاری چند ویجت اختیاری:

JavaScript
const results = await Promise.allSettled([getRepos("octocat"), getEvents("octocat")]);

for (const result of results) {
  if (result.status === "fulfilled") {
    render(result.value);
  } else {
    console.warn("Widget failed:", result.reason);
  }
}

افزودن مهلت زمانی (Timeout)#

fetch() به‌طور پیش‌فرض مهلت زمانی ندارد. در مرورگرهای مدرن و Node.js نسخهٔ ۱۸ به بعد، AbortSignal.timeout() آن را در یک خط به شما می‌دهد:

JavaScript
async function fetchWithTimeout(url, ms = 5000) {
  const response = await fetch(url, { signal: AbortSignal.timeout(ms) });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

اگر درخواست بیش از ms میلی‌ثانیه طول بکشد، پرامیس با یک TimeoutError رد می‌شود که می‌توانید مثل هر خطای دیگری در try/catch مدیریتش کنید.

حلقه‌ها و await#

استفاده از await داخل حلقهٔ for...of آیتم‌ها را یکی‌یکی پردازش می‌کند. این دقیقاً همان چیزی است که وقتی ترتیب مهم است یا نباید یک API را زیر بار ببرید، لازم دارید:

JavaScript
for (const id of orderIds) {
  await processOrder(id); // waits for each one before starting the next
}

وقتی کارها مستقل‌اند، آن‌ها را به پرامیس تبدیل کنید و با هم منتظرشان بمانید:

JavaScript
await Promise.all(orderIds.map((id) => processOrder(id)));

forEach منتظر نمی‌ماند

orderIds.forEach(async (id) => await processOrder(id)) منتظر هیچ چیزی نمی‌ماند، چون forEach پرامیس‌هایی را که تابعِ شما برمی‌گرداند نادیده می‌گیرد. به‌جای آن از for...of یا Promise.all() استفاده کنید.

جمع‌بندی#

  • پرامیس نمایندهٔ یک مقدار در آینده است: در انتظار، انجام‌شده یا ردشده.
  • توابع async همیشه پرامیس برمی‌گردانند؛ await فقط همان تابع را متوقف می‌کند.
  • برای خطاها از try/catch استفاده کنید و یادتان باشد با fetch() مقدار response.ok را بررسی کنید.
  • کارهای مستقل را با Promise.all() موازی اجرا کنید، نه اینکه یکی‌یکی منتظرشان بمانید.
  • هرگز به forEach برای منتظر ماندنِ callbackهای ناهمگام تکیه نکنید.

دفعهٔ بعد که await می‌نویسید، از خودتان بپرسید: آیا این خط واقعاً باید منتظر خط بالایی بماند؟ همین یک سؤال برنامه‌هایتان را به‌طور محسوسی سریع‌تر می‌کند.