منابع آموزشی و پژوهشی، برای دانشگاه و مدرسه
راهنمای انتخابمیز نویسنده
آموزش javascriptدرس ۱۵ از ۲۰رایگان

async/await در جاوااسکریپت؛ مدیریت خطا و اجرای هم‌زمان

async/await شکل خواناتری برای کار با Promise است، اما ترتیب شروع عملیات و مدیریت خطا همچنان مهم‌اند. این درس، اجرای پشت‌سرهم و هم‌زمان، Promise.all، allSettled و دام forEach با callback ناهمگام را توضیح می‌دهد.

فضای تمرینبه‌روزرسانی: ۱۴۰۵/۸/۵
کدنویسی ناهمگام و داده
در این درس می‌خوانید

async/await قرارداد تازه‌ای به‌جای Promise نیست؛ روشی برای نوشتن همان منطق با جریان خواناتر است. await ادامهٔ تابع ناهمگام را تا نتیجهٔ Promise به تعویق می‌اندازد، اما کل برنامه را متوقف نمی‌کند. همچنین تابع async، محاسبهٔ سنگین را خودکار به worker منتقل نمی‌کند. در درس پانزدهم، با دادهٔ شبیه‌سازی‌شده می‌بینیم چه وقت عملیات باید منتظر هم بمانند و چه وقت می‌توانند مستقل شروع شوند.

تابع async همیشه Promise برمی‌گرداند

javascript
async function lessonCount() {
  return 20;
}
lessonCount().then(value => console.log(value)); // 20
console.log(lessonCount() instanceof Promise); // true

اگر مقدار معمولی برگردانید، نتیجه در قرارداد Promise قرار می‌گیرد و اگر خطا پرتاب شود، Promise رد می‌شود. برای دسترسی به مقدار، از await داخل تابع async یا از then استفاده کنید. await در سطح بالای فایل فقط در محیط و ماژول مناسب مجاز است؛ برای سازگاری نمونه‌های این درس، کار را داخل تابع async قرار می‌دهیم و فراخوانی نهایی را نیز با مسیر catch انجام می‌دهیم.

یک ابزار شبیه‌سازی کوچک

javascript
function delayValue(value, milliseconds, fail = false) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (fail) reject(new Error("Simulated failure"));
      else resolve(value);
    }, milliseconds);
  });
}

این تابع فقط آزمایش ترتیب و خطاست و سرویس واقعی نیست. زمان تایمر تضمین دقیق زمان تکمیل نیست. جداکردن شبیه‌سازی از شبکه کمک می‌کند علت رفتار را بدون نوسان اینترنت ببینیم. هنگام استفاده از API واقعی، قرارداد لغو، timeout و خطای HTTP باید جداگانه بررسی شود؛ درس بعد این مرز را با fetch کامل می‌کند.

اجرای ترتیبی وقتی وابستگی وجود دارد

javascript
async function sequentialExample() {
  const user = await delayValue({ id: 7 }, 20);
  const message = await delayValue(`درس‌های کاربر ${user.id}`, 20);
  return message;
}
sequentialExample().then(console.log).catch(console.error);

مرحلهٔ دوم به شناسهٔ مرحلهٔ اول وابسته است، پس انتظار منطقی است. اما اگر دو عملیات مستقل باشند، نوشتن دو await پشت‌سرهم، شروع دومی را بی‌دلیل عقب می‌اندازد. نکتهٔ اصلی زمان شروع عملیات است؛ await فقط محل انتظار را نشان می‌دهد. کد را از روی وابستگی داده طراحی کنید و برای کم‌کردن چند خط، عملیات وابسته را هم‌زمان نکنید.

Promise.all برای نتیجهٔ کاملِ چند کار مستقل

javascript
async function dashboardExample() {
  const [title, count] = await Promise.all([
    delayValue("جاوااسکریپت", 30),
    delayValue(20, 10)
  ]);
  return { title, count };
}
dashboardExample().then(console.log).catch(console.error);
// { title: "جاوااسکریپت", count: 20 }

هر دو فراخوانی هنگام ساخت آرایه شروع می‌شوند. ترتیب نتیجه‌های Promise.all مطابق ترتیب ورودی است، نه ترتیب پایان. اگر یکی رد شود، Promise ترکیبی رد می‌شود؛ عملیات دیگر خودکار لغو نمی‌شوند و ممکن است همچنان کار کنند. اگر درخواست‌ها منبع محدود دارند، باید محدودیت هم‌زمانی و امکان لغو را آگاهانه طراحی کنید. Promise.all روی یک آرایهٔ بزرگ، خودش سقف تعداد کار فعال نمی‌گذارد.

allSettled برای گزارش موفقیت و شکست جزئی

javascript
async function partialResults() {
  const results = await Promise.allSettled([
    delayValue("بخش اول", 10),
    delayValue("بخش دوم", 10, true),
    delayValue("بخش سوم", 10)
  ]);
  console.log(results.map(result => result.status));
  // ["fulfilled", "rejected", "fulfilled"]
}
partialResults().catch(console.error);

allSettled وقتی مناسب است که دیدن نتیجهٔ هر کار، حتی در صورت شکست دیگری، ارزش دارد. هر نتیجه status دارد و با توجه به آن value یا reason را می‌خوانید. برای کاری که وجود تمام داده‌ها ضروری است، صرفاً نمایش بخش موفق ممکن است تصمیم نادرست ایجاد کند. ابتدا قاعدهٔ محصول را مشخص کنید: آیا صفحه با اطلاعات ناقص قابل استفاده است یا باید خطای کلی بدهد؟

خطا و پاک‌سازی با try/catch/finally

javascript
async function runExample() {
  let loading = true;
  try {
    const title = await delayValue("درس آماده است", 10);
    console.log(title);
  } catch (error) {
    console.log("آماده‌سازی درس انجام نشد");
  } finally {
    loading = false;
    console.log("loading:", loading); // false
  }
}
runExample().catch(console.error);

finally برای بازگرداندن وضعیت‌هایی مانند loading مناسب است. آن را با return یا throw غیرضروری جایگزین نتیجهٔ اصلی نکنید. همچنین Array.forEach منتظر Promise برگشتی callbackهای async نمی‌ماند؛ برای کار ترتیبی از for...of همراه await و برای کارهای مستقلِ محدود از map همراه Promise.all استفاده کنید. اگر اثر جانبی مانند ذخیرهٔ داده دارید، حتی با ترتیب درست، سیاست تکرار درخواست و شکست جزئی نیز لازم است.

تمرین و معیار پاسخ

سه عملیات شبیه‌سازی‌شده بسازید که یکی شکست بخورد. یک‌بار آن‌ها را با Promise.all و بار دیگر با allSettled اجرا کنید. در حالت اول باید خطای ترکیبی را در catch ببینید؛ در حالت دوم هر سه وضعیت را بخوانید. سپس توضیح دهید چرا ردشدن Promise.all به معنای متوقف‌شدن دو تایمر دیگر نیست. برای نسخهٔ دوم می‌توانید از کد partialResults استفاده و جای عملیات شکست‌خورده را تغییر دهید؛ ترتیب statusها باید همچنان با ورودی یکسان بماند.

منابع انگلیسی

سناریوهای آموزشی برای این مجموعه نوشته شده‌اند. در درس بعد، درخواست واقعی را با fetch، بررسی وضعیت پاسخ و لغو با AbortController انجام می‌دهیم.

پیش‌نیاز و محیط اجرای مثال‌ها

درس 15 از ۲۰؛ پیش‌نیاز: درس‌های قبلی مجموعه. در مرورگر به‌روز یا Node.js LTS. قطعه‌های مثال را در یک فایل مستقل به ترتیب بگذارید؛ تابع delayValue برای شبیه‌سازی است و درخواست شبکه نمی‌فرستد.

پیشرفت روی همین مرورگر ذخیره می‌شود.

یکجا انتخاب کن، راحت‌تر دریافت کن

انتخاب‌های من ۰ منبع

مجموعهٔ مطالعه‌ات را بساز.

کنار مقاله‌ها «انتخاب» را بزنید تا چند فایل را با هم دریافت کنید.

انتخاب مقاله‌های رایگان