Async/Await: نوشتن کد ناهمزمان به سبک همزمان

در درس‌های قبل با Promise و زنجیره‌ی then/catch آشنا شدید که مشکل تودرتویی Callbackها را حل می‌کند. اما با افزایش تعداد مراحل، حتی زنجیره‌ی then نیز می‌تواند طولانی و کمی دشوارخوان شود. async/await نحوه‌ی جدیدی برای کار با همان Promiseهاست که به کد اجازه می‌دهد ظاهری کاملاً شبیه کد Synchronous داشته باشد، بدون آنکه هیچ مزیت ناهمزمان بودن از دست برود.

تابع async

با قرار دادن کلیدواژه‌ی async پیش از تعریف یک تابع، آن تابع همیشه یک Promise برمی‌گرداند، حتی اگر داخل آن به‌صراحت هیچ Promise ای ساخته نشده باشد:

async function greet() {
  return "سلام";
}

const result = greet();
console.log(result); // Promise { "سلام" }

result.then((value) => console.log(value)); // "سلام"

اگر داخل یک تابع async مقداری با return برگردانده شود، آن مقدار به‌طور خودکار در یک Promise resolve‌شده قرار می‌گیرد. اگر خطایی throw شود، تابع Promise ای reject‌شده برمی‌گرداند، درست مانند فراخوانی reject در سازنده‌ی Promise.

کلیدواژه‌ی await

کلیدواژه‌ی await فقط داخل توابع async قابل استفاده است و اجرای تابع را تا زمان settle شدن Promise موردنظر متوقف می‌کند، بدون آنکه بقیه‌ی برنامه را مسدود کند:

بازنویسی زنجیره‌ی then با await
خروجی کنسول اینجا نمایش داده می‌شود...

همان‌طور که مشاهده می‌کنید، این کد دقیقاً همان کاری را انجام می‌دهد که زنجیره‌ی then در درس ۲۱-۲ انجام می‌داد، اما ظاهر آن کاملاً شبیه کد Synchronous خطی است؛ هر خط منتظر پایان خط قبلی می‌ماند، بدون نیاز به تعریف تابع callback جداگانه برای هر مرحله.

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

از آنجا که async/await کد را شبیه Synchronous می‌کند، مدیریت خطا نیز با همان ساختار آشنای try/catch انجام می‌شود، به‌جای متد catch زنجیره‌ای:

async function getUserOrders(userId) {
  try {
    const userResponse = await fetch(`/api/user/${userId}`);

    if (!userResponse.ok) {
      throw new Error(`خطای دریافت کاربر: ${userResponse.status}`);
    }

    const user = await userResponse.json();
    const ordersResponse = await fetch(`/api/orders?userId=${user.id}`);
    const orders = await ordersResponse.json();

    return orders;
  } catch (error) {
    console.log("خطایی رخ داد:", error.message);
    return [];
  }
}

درست مانند catch در زنجیره‌ی Promise، این بلوک try/catch خطای هر یک از مراحل await شده—چه در اولین fetch و چه در آخرین آن—را می‌گیرد، بدون نیاز به نوشتن بلوک جداگانه برای هر مرحله.

جدول مقایسه‌ی then/catch و async/await

ویژگی then/catch async/await
ساختار نوشتاری زنجیره‌ای، با تابع callback در هر مرحله خطی و شبیه کد Synchronous
مدیریت خطا متد catch در انتهای زنجیره بلوک try/catch آشنا
مقدار بازگشتی تابع همیشه یک Promise (به‌طور طبیعی) همیشه یک Promise (به‌صورت خودکار توسط async)
خوانایی در مراحل زیاد ممکن است طولانی و پرتکرار شود معمولاً خواناتر و نزدیک‌تر به منطق خطی

نکته‌ی مهم: async/await جایگزین Promise نیست

یکی از تصورات نادرست رایج این است که async/await روشی کاملاً متفاوت و جایگزین Promise است. در واقع، async/await فقط یک لایه‌ی نحوی (syntactic sugar) روی همان Promiseهاست؛ هر await در پس‌زمینه دقیقاً همان کاری را انجام می‌دهد که then انجام می‌داد، و درک صحیح آن بدون شناخت Promise و State های آن (که در درس ۲۱-۱ بررسی شد) ممکن نیست.

نکات کلیدی

  • تابع async همیشه یک Promise برمی‌گرداند، حتی اگر مقدار return شده یک Promise نباشد.
  • await فقط داخل توابع async قابل استفاده است و اجرای همان تابع را تا settle شدن Promise متوقف می‌کند.
  • مدیریت خطا در async/await از طریق try/catch انجام می‌شود، نه متد catch زنجیره‌ای.
  • async/await جایگزین Promise نیست، بلکه لایه‌ی نحوی خواناتری روی همان مفهوم است.

جمع‌بندی

async/await با ترکیب دو کلیدواژه‌ی ساده، امکان نوشتن کد ناهمزمان را به شکلی خطی و شبیه کد Synchronous فراهم می‌کند، بدون آنکه هیچ‌کدام از مزایای غیرمسدودکننده‌ی Promise از بین برود. این ساختار در پروژه‌های واقعی، به‌خصوص در سناریوهایی با چند مرحله‌ی متوالی، معمولاً خواناتر از زنجیره‌ی then/catch است. در درس بعدی، نحوه‌ی ترکیب async/await با متدهایی مثل Promise.all برای اجرای موازی چند عملیات مستقل بررسی خواهد شد.