نحوه‌ی نگارش async/await در جاوااسکریپت قرار بود ما را از جهنم کال‌بک (callback hell) نجات دهد. اما در عوض، مشکلی آرام‌تر و موذیانه‌تر را معرفی کرد: کدی که درست به نظر می‌رسد اما رفتاری غیرقابل پیش‌بینی دارد. شما await را در بدنه تابع می‌بینید و تصور می‌کنید همه چیز مودبانه و خط به خط متوقف می‌شود. اما اغلب این‌طور نیست. حلقه‌ها با سرعت جلو می‌روند. کل دسته‌ها (batches) به دلیل یک درخواست ناموفق از هم می‌پاشند. فایل‌های ورودی (entry files) بدون دلیل، پوشش‌های (wrappers) async زشتی پیدا می‌کنند. اگر با هر یک از این موارد برخورد کرده‌اید، این سه الگو آن‌ها را اصلاح می‌کنند.

استفاده از await داخل forEach را متوقف کنید

در اینجا یک اشتباه رایج وجود دارد که در ظاهر بی‌ضرر به نظر می‌رسد:

const urls = ['/api/user', '/api/posts', '/api/comments'];

urls.forEach(async (url) => {
  const res = await fetch(url);
  const data = await res.json();
  console.log(data);
});

console.log('All done!');

این کد را اجرا کنید؛ می‌بینید که 'All done!' قبل از اینکه حتی یک پاسخ برگردد، چاپ می‌شود. چرا؟ forEach تابع کال‌بک را برای هر عنصر بلافاصله اجرا می‌کند. این تابع منتظر پرامیس (promise) داخل هر تکرار نمی‌ماند. کلمه کلیدی async هر کال‌بک را به یک پرامیس تبدیل می‌کند که forEach فوراً آن را نادیده می‌گیرد. حلقه شما در چند میکروثانیه تمام می‌شود، در حالی که درخواست‌های شبکه هر کدام به مسیر خود می‌روند. اگر نیاز دارید خطاها به ترتیب مدیریت شوند، یا اگر می‌خواهید تضمین کنید که یک درخواست قبل از شروع درخواست بعدی تمام می‌شود، این الگو هر دو تضمین را بی‌سروصدا از بین می‌برد.

آن را با یک حلقه for...of جایگزین کنید:

const urls = ['/api/user', '/api/posts', '/api/comments'];

for (const url of urls) {
  const res = await fetch(url);
  const data = await res.json();
  console.log(data);
}

console.log('All done!');

حالا حلقه واقعاً در هر await متوقف می‌شود. درخواست دوم منتظر درخواست اول می‌ماند. 'All done!' تنها پس از اتمام همه چیز چاپ می‌شود.

از for...of زمانی استفاده کنید که ترتیب اهمیت دارد؛ مثلاً آپلود فایل‌ها یکی‌یکی برای رعایت محدودیت نرخ (rate limits)، نوشتن ردیف‌های پایگاه داده با ترتیبی خاص، یا زنجیره‌سازی فراخوانی‌های API که در آن درخواست بعدی به داده‌های پاسخ قبلی نیاز دارد. اگر واقعاً اجرای موازی (parallel execution) می‌خواهید، با forEach کلنجار نروید. صراحتاً از Promise.all استفاده کنید تا قصد شما برای توسعه‌دهنده بعدی قابل درک باشد. اما هرگز await و forEach را با انتظار رفتار همگام (synchronous) با هم ترکیب نکنید. این اتفاق نخواهد افتاد.

زمانی که "صفر" نمی‌تواند پاسخ باشد، از Promise.allSettled استفاده کنید

Promise.all از نظر معنایی صادق است. آرایه‌ای از پرامیس‌ها را به آن بدهید و آرایه‌ای از نتایج را برمی‌گرداند. نکته اینجاست که به محض اینکه حتی یک پرامیس رد (reject) شود، کل عملیات بلافاصله رد می‌شود. بقیه پرامیس‌های در جریان رها می‌شوند تا خودشان تمام شوند، اما شما دسترسی به نتایج آن‌ها را از دست می‌دهید. در محیط عملیاتی (production)، این رفتار "همه یا هیچ" دردسرساز است.

تصور کنید اپلیکیشن شما ویجت‌های یک داشبورد را از چهار سرویس مستقل دریافت می‌کند: تحلیل ترافیک، داده‌های درآمد، بازخورد کاربران و سلامت سرور. API مربوط به درآمد با یک تایم‌اوت (timeout) کوتاه مواجه می‌شود. در حالت Promise.all کل داشبورد شما با خطا مواجه می‌شود. آن سه پاسخ سالم در خلاء گم می‌شوند. کاربر ابتدا یک آیکون در حال بارگذاری (spinner) و سپس صفحه خطا را می‌بیند، فقط به این دلیل که یک چهارم داده‌ها دچار مشکل شده‌اند.

Promise.allSettled قرارداد منطقی‌تری به شما می‌دهد. این متد منتظر می‌ماند تا تک‌تک پرامیس‌ها، به هر شکلی که باشد، تمام شوند. مقدار بازگشتی (resolved value) آرایه‌ای از اشیاء است که هر نتیجه را توصیف می‌کند:

const requests = [
  fetch('/api/traffic'),
  fetch('/api/revenue'),
  fetch('/api/feedback'),
  fetch('/api/health')
];

const results = await Promise.allSettled(requests);

results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    renderWidget(index, result.value);
  } else {
    renderError(index, result.reason);
  }
});

هیچ پاسخی دور ریخته نمی‌شود. شما آنچه را که می‌توانید نمایش می‌دهید و خطا را ایزوله (isolate) می‌کنید. این الگو زمانی اهمیت دارد که با عملیات‌های غیرمرتبط سر و کار دارید؛ مانند ارسال اعلان‌های انبوه، ارسال webhookهای شخص ثالث، یا وارد کردن رکوردها از چندین جریان CSV. شما همچنان به ردیابی متمرکز خطاها نیاز دارید، اما اپلیکیشن شما پایداری خود را حفظ می‌کند.

یک نکته کاربردی: allSettled مجموعه کامل را برمی‌گرداند، بنابراین همچنان باید نتایج را بررسی کنید و تصمیم بگیرید که "موفقیت نسبی" برای قابلیت (feature) شما به چه معناست. با آرایه بازگشتی به عنوان داده‌های کاملاً موفق برخورد نکنید. قبل از اینکه چیزی را به لایه وضعیت (state layer) خود اضافه کنید، فیلدهای وضعیت (status) را چک کنید.

از Top-level await استفاده کنید و Wrapper IIFE را حذف کنید

سال‌ها اگر می‌خواستید چیزی را در ریشه (root) یک فایل await کنید، آن را در یک تابع async که بلافاصله فراخوانی می‌شود (IIFE) قرار می‌دادید:

(async () => {
  const config = await loadConfig();
  startServer(config);
})();

این روش کار می‌کند، اما باعث شلوغی کد می‌شود. top-level await که در ES modules به صورت بومی (native) وجود دارد، به شما اجازه می‌دهد از این کدهای تکراری و اضافی (boilerplate) بی‌نیاز شوید:

const config = await loadConfig();
startServer(config);

از این قابلیت در نقطه ورود (entry point) اپلیکیشن خود یا در ماژول‌های پیکربندی اختصاصی استفاده کنید، جایی که مقداردهی اولیه (initialization) باید قبل از اجرای هر چیز دیگری کامل شود. بارگذاری فایل‌های محیطی (environment files)، ایجاد یک استخر اتصال (connection pool) پایگاه داده، یا دریافت پرچم‌های ویژگی (feature flags) از راه دور، همگی موارد مناسبی هستند. از آنجایی که top-level await اجرای گراف ماژول را متوقف می‌کند (فایل‌های دیگری که این فایل را وارد می‌کنند، منتظر حل شدن پرامیس شما می‌مانند)، شما یک وضعیت تضمین‌شده خواهید داشت. بقیه کد شما می‌تواند db را وارد (import) کند و مطمئن باشد که اتصال از قبل برقرار شده است.

There are two catches. First, your runtime or bundler must support ES modules. In Node.js, that means either using .mjs extensions or setting "type": "module" in your package.json. Second, because waiting at the module level delays every importer, keep the awaited work focused. Heavy sequential fetches at the top of a frequently imported utility file will slow down your entire application cold start. Reserve top-level await for true bootstrap tasks that other modules genuinely depend upon.

What Actually Changes When You Adopt These Patterns

Predictability is the first payoff. When you read a for...of loop, you know exactly when the block underneath finishes. There are no ghost promises racing behind the scenes, no foreach callbacks detaching from your error handlers. Your control flow matches the shape of the code on the screen.

Resilience comes next. Promise.allSettled forces you to think about partial failure instead of hoping every external system stays perfect. Production software is not binary. Some endpoints will flake. Some file reads will hit permission errors. Designing for the reality of scattered failures keeps your application upright without sweeping legitimate data under the rug.

Clarity ties it together. for...of reads like plain English progression. allSettled states its intent in its name. Top-level await removes cryptic IIFE wrappers so your entry files start with business logic instead of syntactic acrobatics. The next engineer who touches the file—whether that is you in six months or a teammate on a deadline—will thank you.

A Real Takeaway

Do not treat async/await as a global fix that you sprinkle over existing code. Audit your current projects for these three specific anti-patterns. Search for await inside forEach blocks and replace them with for...of or an intentional Promise.all. Review every Promise.all that talks to external services and ask whether a single failure should really torpedo the entire operation; if not, switch to Promise.allSettled and handle the mixed results. Finally, strip the async IIFEs out of your ES module entry points and let top-level await handle your bootstrap sequence directly. These are small mechanical changes, but together they turn fragile asynchronous scripts into code you can actually trust.