JavaScript का async/await सिंटैक्स हमें callback hell से बचाने के लिए बनाया गया था। इसके बजाय, इसने एक शांत लेकिन अधिक घातक समस्या पेश की: ऐसा कोड जो दिखने में तो सही लगता है लेकिन व्यवहार अनिश्चित (unpredictable) होता है। आप फ़ंक्शन बॉडी में await को देखते हैं और मान लेते हैं कि सब कुछ शालीनता से, एक-एक लाइन करके रुक जाएगा। अक्सर ऐसा नहीं होता। लूप्स तेज़ी से आगे बढ़ जाते हैं। एक सिंगल फेल हुए रिक्वेस्ट की वजह से पूरा बैच क्रैश हो जाता है। बिना किसी कारण के एंट्री फाइल्स में अजीबोगरीब async wrappers उग आते हैं। यदि आप इनमें से किसी का भी सामना कर चुके हैं, तो ये तीन पैटर्न उन्हें ठीक कर देंगे।

forEach के अंदर await का उपयोग करना बंद करें

यहाँ एक सामान्य गलती है जो देखने में तो बेअसर लगती है:

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 हर एलिमेंट के लिए कॉलबैक को तुरंत निष्पादित (execute) करता है। यह प्रत्येक इटरेशन के अंदर के प्रॉमिस (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 का उपयोग तब करें जब सीक्वेंस (sequence) मायने रखता हो—जैसे रेट लिमिट्स का सम्मान करने के लिए एक बार में एक फ़ाइल अपलोड करना, एक विशिष्ट क्रम में डेटाबेस रोज़ (rows) लिखना, या API कॉल्स को चेन करना जहाँ अगली रिक्वेस्ट को पिछली रिस्पॉन्स से डेटा चाहिए। यदि आप वास्तव में पैरेलल एक्जीक्यूशन (parallel execution) चाहते हैं, तो forEach के साथ उलझें नहीं। स्पष्ट रूप से Promise.all का उपयोग करें ताकि आपका इरादा अगले डेवलपर को स्पष्ट रूप से दिखाई दे। लेकिन सिंक्रोनस व्यवहार (synchronous behavior) की उम्मीद में await और forEach को कभी न मिलाएं। ऐसा नहीं होगा।

Promise.allSettled का उपयोग करें जब 'शून्य' (Zero) समाधान न हो

Promise.all सिमेंटिक रूप से ईमानदार है। इसे प्रॉमिस का एक ऐरे (array) दें, और यह परिणामों का एक ऐरे लौटाता है। पेच यह है कि जैसे ही कोई एक प्रॉमिस रिजेक्ट होता है, पूरी चीज़ तुरंत रिजेक्ट हो जाती है। बाकी सभी पेंडिंग प्रॉमिस अपने आप खत्म होने के लिए छोड़ दिए जाते हैं, लेकिन आप उनके परिणामों तक पहुँच खो देते हैं। प्रोडक्शन में, यह 'सब कुछ या कुछ भी नहीं' (all-or-nothing) वाला व्यवहार काफी नुकसानदेह होता है।

कल्पना करें कि आपका एप्लिकेशन चार स्वतंत्र सेवाओं से डैशबोर्ड के विजेट्स (widgets) फेच करता है: ट्रैफिक एनालिटिक्स, रेवेन्यू डेटा, यूजर फीडबैक और सर्वर हेल्थ। रेवेन्यू API में एक छोटा सा टाइमआउट हो जाता है। Promise.all के तहत, आपका पूरा डैशबोर्ड एरर थ्रो कर देता है। तीन स्वस्थ रिस्पॉन्स शून्य में गायब हो जाते हैं। यूजर को एक स्पिनर दिखता है, और फिर एक फेलियर स्क्रीन, क्योंकि डेटा का एक चौथाई हिस्सा सही से काम नहीं कर पाया।

Promise.allSettled आपको एक बेहतर विकल्प देता है। यह तब तक इंतज़ार करता है जब तक कि हर एक प्रॉमिस खत्म न हो जाए, चाहे वह किसी भी तरह से खत्म हो। रिज़ॉल्व की गई वैल्यू ऑब्जेक्ट्स का एक ऐरे होती है जो प्रत्येक परिणाम का वर्णन करती है:

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);
  }
});

कोई भी रिस्पॉन्स फेंका नहीं जाता। आप जो संभव है उसे रेंडर करते हैं और विफलता (failure) को अलग कर देते हैं। यह पैटर्न तब महत्वपूर्ण होता है जब आप असंबंधित ऑपरेशन्स (unrelated operations) के साथ काम कर रहे हों—जैसे बल्क नोटिफिकेशन, थर्ड-पार्टी वेबहुक डिस्पैच, या कई CSV स्ट्रीम से रिकॉर्ड इम्पोर्ट करना। आपको अभी भी सेंट्रलाइज्ड एरर ट्रैकिंग की आवश्यकता होगी, लेकिन आपका एप्लिकेशन स्थिर रहेगा।

एक व्यावहारिक बात: allSettled पूरा सेट लौटाता है, इसलिए आपको अभी भी परिणामों को छानना होगा और यह तय करना होगा कि आपके फीचर के लिए "आंशिक सफलता" (partial success) का क्या अर्थ है। लौटाए गए ऐरे को एक समान रूप से सफल डेटा के रूप में न मानें। अपनी स्टेट लेयर (state layer) में कुछ भी पुश करने से पहले उन स्टेटस फ़ील्ड्स की जाँच करें।

Top-Level await घोषित करें और Wrapper IIFE को खत्म करें

सालों से, यदि आप किसी फ़ाइल के रूट पर किसी चीज़ का इंतज़ार (await) करना चाहते थे, तो आप उसे एक इमीडिएटली इन्वोक्ड (immediately invoked) async फ़ंक्शन में लपेट देते थे:

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

यह काम तो करता है, लेकिन यह अनावश्यक शोर (noise) है। ES modules में नेटिव रूप से उपलब्ध Top-level await, आपको इस औपचारिक बॉयलरप्लेट (boilerplate) को हटाने की अनुमति देता है:

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

इसका उपयोग अपने एप्लिकेशन के एंट्री पॉइंट पर या समर्पित कॉन्फ़िगरेशन मॉड्यूल में करें जहाँ किसी भी अन्य चीज़ के चलने से पहले इनिशियलाइज़ेशन (initialization) पूरा होना चाहिए। एनवायरनमेंट फ़ाइल्स लोड करना, डेटाबेस कनेक्शन पूल स्थापित करना, या रिमोट फीचर फ्लैग्स फेच करना, ये सभी इसके लिए उपयुक्त उदाहरण हैं। क्योंकि top-level await मॉड्यूल ग्राफ के निष्पादन (execution) को ब्लॉक करता है—इस फ़ाइल को इम्पोर्ट करने वाली अन्य फ़ाइलें आपके प्रॉमिस के रिज़ॉल्व होने का इंतज़ार करेंगी—आपको एक गारंटीड स्टेट (guaranteed state) मिलती है। आपका बाकी कोडबेस db को इम्पोर्ट कर सकता है और जान सकता है कि कनेक्शन पहले से ही लाइव है।

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.