مقالات › جاوااسکریپت

آموزش کامل Promise و async/await در جاوااسکریپت

جاوااسکریپت ناهمگام را اصولی یاد بگیرید: Callback، Promise، زنجیره‌سازی، مدیریت خطا، async/await، تفاوت Promise.all و allSettled و race، لغو با AbortController و اشتباهات رایج.

جاوااسکریپت روی یک Thread اجرا می‌شود، اما درخواست‌های شبکه، تایمرها و ورودی کاربر را بدون قفل شدن مدیریت می‌کند. راز آن برنامه‌نویسی ناهمگام است و ابزارهای مدرنش Promise و async/await هستند. در این راهنما این مفاهیم را از پایه می‌سازیم و با الگوهایی که در پروژه‌های واقعی استفاده می‌کنید تمام می‌کنیم.

از Callback تا Promise

در ابتدا APIهای ناهمگام Callback می‌گرفتند:

getUser(1, (err, user) => {
  if (err) return handle(err);
  getOrders(user.id, (err, orders) => {
    if (err) return handle(err);
    getInvoice(orders[0].id, (err, invoice) => {
      // … «هرم عذاب»
    });
  });
});

Callbackهای تودرتو سخت خوانده می‌شوند، مدیریت خطا همه‌جا تکرار می‌شود و صدا زدن دوباره‌ی یک Callback به‌راحتی رخ می‌دهد. Promise همه‌ی این‌ها را حل می‌کند.

Promise چیست؟

Promise شیئی است که مقداری را نشان می‌دهد که بعداً در دسترس خواهد بود. همیشه در یکی از این سه وضعیت است:

  • pending — عملیات هنوز تمام نشده،
  • fulfilled — با یک مقدار کامل شده،
  • rejected — با یک دلیل (معمولاً یک Error) شکست خورده.

Promise بعد از تعیین وضعیت (fulfilled یا rejected) دیگر هرگز تغییر نمی‌کند.

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const loadConfig = () =>
  new Promise((resolve, reject) => {
    const raw = localStorage.getItem("config");
    raw ? resolve(JSON.parse(raw)) : reject(new Error("No config found"));
  });

به‌ندرت خودتان new Promise می‌نویسید؛ بیشتر APIها (fetch، کلاینت‌های دیتابیس، APIهای فایل) از قبل Promise برمی‌گردانند.

استفاده از Promise با then، catch و finally

fetch("/api/users/1")
  .then((response) => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();          // برگرداندن Promise زنجیره را ادامه می‌دهد
  })
  .then((user) => console.log(user.name))
  .catch((error) => console.error("Failed:", error.message))
  .finally(() => hideSpinner());

قوانین کلیدی:

  • هر چیزی که از then برگردانید، مقدار then بعدی می‌شود. برگرداندن Promise یعنی منتظر آن ماندن.
  • خطای پرتاب‌شده یا Promise ردشده مستقیم به نزدیک‌ترین catch می‌رود.
  • finally در هر صورت اجرا می‌شود؛ مناسب کارهای پاک‌سازی.

هشدار: fetch روی خطاهای HTTP مثل 404 یا 500 reject نمی‌شود. فقط وقتی شبکه قطع باشد reject می‌شود. همیشه response.ok را بررسی کنید.

async/await؛ Promiseهایی که مثل کد همگام خوانده می‌شوند

async/await نحوی است که روی Promise ساخته شده:

async function loadUserWithOrders(id) {
  const userResponse = await fetch(`/api/users/${id}`);
  if (!userResponse.ok) throw new Error("User not found");
  const user = await userResponse.json();

  const ordersResponse = await fetch(`/api/users/${id}/orders`);
  const orders = await ordersResponse.json();

  return { ...user, orders };
}
  • تابع async همیشه یک Promise برمی‌گرداند.
  • await اجرای همان تابع (نه کل برنامه!) را تا تعیین وضعیت Promise متوقف می‌کند.
  • خطاها با try/catch معمولی مدیریت می‌شوند:
try {
  const data = await loadUserWithOrders(1);
  render(data);
} catch (error) {
  showToast(error.message);
} finally {
  setLoading(false);
}

ترتیبی یا موازی

await کردن عملیات مستقل یکی‌یکی وقت را هدر می‌دهد:

// ❌ حدود ۳ ثانیه، اگر هرکدام ۱ ثانیه طول بکشد
const user = await getUser(id);
const posts = await getPosts(id);
const followers = await getFollowers(id);

همه را با هم شروع کنید و گروهی منتظر بمانید:

// ✅ حدود ۱ ثانیه
const [user, posts, followers] = await Promise.all([
  getUser(id),
  getPosts(id),
  getFollowers(id),
]);

ترکیب‌کننده‌های Promise

متد وقتی resolve می‌شود وقتی reject می‌شود کاربرد
Promise.all همه موفق شوند (آرایه‌ی مقادیر) هرکدام شکست بخورد (شکست سریع) درخواست‌هایی که همه باید موفق باشند
Promise.allSettled همه تمام شوند (آرایه‌ی {status, value/reason}) هرگز کارهای مستقل که موفقیت جزئی قابل قبول است
Promise.race اولین تمام شود اولین شکست بخورد Timeout
Promise.any اولین موفق شود همه شکست بخورند (AggregateError) سریع‌ترین از چند سرور
const results = await Promise.allSettled(urls.map((u) => fetch(u)));
const ok = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
const failed = results.filter((r) => r.status === "rejected");

یک تابع کمکی برای Timeout

function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timed out after ${ms} ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

لغو عملیات با AbortController

Promise به‌تنهایی قابل لغو نیست، اما بسیاری از APIها یک AbortSignal می‌پذیرند:

const controller = new AbortController();

fetch("/api/search?q=react", { signal: controller.signal })
  .then((r) => r.json())
  .then(showResults)
  .catch((err) => {
    if (err.name === "AbortError") return; // مورد انتظار
    console.error(err);
  });

controller.abort(); // مثلاً کاربر عبارت جدیدی تایپ کرد

مرورگرهای مدرن AbortSignal.timeout(5000) را هم برای Timeout داخلی فراهم می‌کنند.

پردازش آیتم‌ها با هم‌زمانی محدود

Promise.all روی ۱۰۰۰ آدرس، ۱۰۰۰ درخواست را یک‌جا می‌فرستد. به‌جای آن دسته‌ای پردازش کنید:

async function mapInBatches(items, size, fn) {
  const results = [];
  for (let i = 0; i < items.length; i += size) {
    const batch = items.slice(i, i + size);
    results.push(...(await Promise.all(batch.map(fn))));
  }
  return results;
}

await mapInBatches(imageUrls, 5, uploadImage);

اشتباهات رایج

  1. فراموش کردن await — به‌جای مقدار یک Promise می‌گیرید و خطاها به Rejectionهای مدیریت‌نشده تبدیل می‌شوند.
  2. await داخل forEach — forEach به Promiseهای برگشتی اهمیتی نمی‌دهد. برای کار ترتیبی از for...of و برای کار موازی از Promise.all(items.map(...)) استفاده کنید.
// ❌ منتظر نمی‌ماند
items.forEach(async (item) => await save(item));

// ✅ ترتیبی
for (const item of items) await save(item);

// ✅ موازی
await Promise.all(items.map(save));
  1. ترکیب تصادفی then و await — در هر تابع یک سبک را انتخاب کنید.
  2. قورت دادن خطاها — catch {} خالی مشکلات واقعی را پنهان می‌کند. لاگ کنید یا دوباره پرتاب کنید.
  3. Rejectionهای مدیریت‌نشده — هر زنجیره‌ی Promise جایی به catch نیاز دارد؛ در Node.js یک Rejection مدیریت‌نشده می‌تواند پروسه را از کار بیندازد.

پشت صحنه چه اتفاقی می‌افتد؟

Callbackهای Promise به‌صورت Microtask اجرا می‌شوند. بعد از تمام شدن کد همگام فعلی، Event Loop قبل از رسیدگی به تایمر یا رویداد بعدی، صف Microtaskها را خالی می‌کند:

console.log("1");
setTimeout(() => console.log("4"), 0);          // Macrotask
Promise.resolve().then(() => console.log("3")); // Microtask
console.log("2");
// 1, 2, 3, 4

فهمیدن این ترتیب، خیلی از معماهای «چرا این زودتر اجرا شد؟» را حل می‌کند.

جمع‌بندی

Promise مقادیر آینده را نمایش می‌دهد و اجازه می‌دهد عملیات را با مدیریت خطای تمیز زنجیر کنید. async/await همان کد را مثل منطق ترتیبی معمولی خوانا می‌کند. کارهای مستقل را با Promise.all موازی اجرا کنید، برای سناریوهای دیگر allSettled، race یا any را انتخاب کنید، با AbortController لغو کنید و از await داخل forEach دوری کنید. با این الگوها، جاوااسکریپت ناهمگام قابل پیش‌بینی و نگهداری می‌شود.

پرسش‌های متداول

آیا async/await از then() سریع‌تر است؟

نه، همان سازوکار Promise است. مزیتش خوانایی و مدیریت خطای ساده‌تر است.

آیا می‌توانم await را در بالاترین سطح فایل استفاده کنم؟

بله، در ماژول‌های ES (type="module" در مرورگر و فایل‌های .mjs یا "type": "module" در Node.js).

آیا داخل try/catch باید return await بنویسم؟

داخل بلوک try، نوشتن return await promise تضمین می‌کند Rejection توسط همان catch گرفته شود. بیرون از try، همان return promise کافی است.