JavaScript-ലെ async/await സിന്റാക്സ് നമ്മെ callback hell-ൽ നിന്ന് രക്ഷിക്കാനാണ് ഉദ്ദേശിച്ചിരുന്നത്. എന്നാൽ പകരം, അത് കൂടുതൽ നിശബ്ദവും എന്നാൽ അപകടകരവുമായ ഒരു പ്രശ്നം അവതരിപ്പിച്ചു: ശരിയാണെന്ന് തോന്നുമെങ്കിലും പ്രവചനാതീതമായി പ്രവർത്തിക്കുന്ന കോഡ്. ഫംഗ്ഷൻ ബോഡിയിൽ await കാണുമ്പോൾ, ഓരോ വരിയും കൃത്യമായി നിർത്തിക്കൊണ്ട് എല്ലാം പതുക്കെ നടക്കുമെന്ന് നിങ്ങൾ കരുതിയേക്കാം. പലപ്പോഴും അങ്ങനെ സംഭവിക്കാറില്ല. ലൂപ്പുകൾ വേഗത്തിൽ മുന്നോട്ട് കുതിക്കുന്നു. ഒരു റിക്വസ്റ്റ് പരാജയപ്പെട്ടാൽ പോലും മുഴുവൻ ബാച്ചുകളും തകരാറിലാകുന്നു. കാരണങ്ങളില്ലാതെ എൻട്രി ഫയലുകളിൽ (entry files) മോശമായ 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 ഓരോ എലമെന്റിനും വേണ്ടിയുള്ള callback ഉടൻ തന്നെ പ്രവർത്തിപ്പിക്കുന്നു. ഓരോ ഇറ്ററേഷനിലും (iteration) ഉള്ള പ്രോമിസിന് (promise) അത് കാത്തുനിൽക്കുന്നില്ല. async കീവേഡ് ഓരോ callback-നെയും ഒരു പ്രോമിസ് ആക്കി മാറ്റുന്നു, എന്നാൽ 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!' പ്രിന്റ് ചെയ്യുകയുള്ളൂ.

ക്രമം (sequence) പ്രധാനപ്പെട്ട സാഹചര്യങ്ങളിൽ for...of ഉപയോഗിക്കുക—ഉദാഹരണത്തിന്, rate limits പാലിക്കാൻ ഫയലുകൾ ഓരോന്നായി അപ്‌ലോഡ് ചെയ്യുമ്പോഴോ, ഡാറ്റാബേസ് റോകൾ ഒരു പ്രത്യേക ക്രമത്തിൽ എഴുതുമ്പോഴോ, അല്ലെങ്കിൽ അടുത്ത റിക്വസ്റ്റിന് മുൻപത്തെ റെസ്പോൺസിൽ നിന്നുള്ള ഡാറ്റ ആവശ്യമായ API call ചെയിനിംഗിലോ ഇത് ഉപയോഗിക്കാം. നിങ്ങൾക്ക് സമാന്തരമായി (parallel) പ്രവർത്തിപ്പിക്കണമെന്നുണ്ടെങ്കിൽ, forEach ഉപയോഗിച്ച് സമയം കളയരുത്. പകരം Promise.all വ്യക്തമായി ഉപയോഗിക്കുക, അങ്ങനെ അടുത്ത ഡെവലപ്പർക്ക് നിങ്ങളുടെ ഉദ്ദേശ്യം മനസ്സിലാക്കാൻ സാധിക്കും. എന്നാൽ സിൻക്രണസ് (synchronous) രീതിയിൽ പ്രവർത്തിക്കുമെന്ന് പ്രതീക്ഷിച്ചുകൊണ്ട് await-ഉം forEach-ഉം ഒരിക്കലും കൂട്ടിക്കലർത്തരുത്. അത് സംഭവിക്കില്ല.

'Zero' എന്നത് ഉത്തരമാകാൻ പാടില്ലാത്തപ്പോൾ Promise.allSettled ഉപയോഗിക്കുക

Promise.all എന്നത് അർത്ഥവത്തായ രീതിയിൽ പ്രവർത്തിക്കുന്ന ഒന്നാണ്. പ്രോമിസുകളുടെ ഒരു അറേ (array) നൽകിയാൽ അത് റിസൾട്ടുകളുടെ ഒരു അറേ തിരികെ നൽകുന്നു. എന്നാൽ ഇതിലെ പ്രശ്നം എന്തെന്നാൽ, ഏതെങ്കിലും ഒരു പ്രോമിസ് പരാജയപ്പെട്ടാൽ (reject), ഉടൻ തന്നെ മുഴുവൻ പ്രക്രിയയും പരാജയപ്പെടുന്നു. ബാക്കിയുള്ള പ്രോമിസുകൾ അവയുടെ വഴിക്ക് പൂർത്തിയാകുമെങ്കിലും, അവയുടെ റിസൾട്ടുകൾ നിങ്ങൾക്ക് ലഭിക്കില്ല. പ്രൊഡക്ഷൻ സാഹചര്യങ്ങളിൽ, ഈ 'എല്ലാം അല്ലെങ്കിൽ ഒന്നുമില്ല' (all-or-nothing) എന്ന രീതി വലിയ ബുദ്ധിമുട്ടുണ്ടാക്കും.

നിങ്ങളുടെ ആപ്ലിക്കേഷൻ ഒരു ഡാഷ്‌ബോർഡിലെ വിഡ്ജറ്റുകൾ (widgets) നാല് സ്വതന്ത്ര സർവീസുകളിൽ നിന്ന് എടുക്കുന്നു എന്ന് കരുതുക: ട്രാഫിക് അനലിറ്റിക്സ്, റെവന്യൂ ഡാറ്റ, യൂസർ ഫീഡ്ബാക്ക്, സെർവർ ഹെൽത്ത് എന്നിവയാണവ. റെവന്യൂ API ഒരു ചെറിയ ടൈമൗട്ട് (timeout) നേരിടുന്നു. Promise.all ഉപയോഗിക്കുകയാണെങ്കിൽ, നിങ്ങളുടെ മുഴുവൻ ഡാഷ്‌ബോർഡും ഒരു എറർ കാണിക്കും. ബാക്കിയുള്ള മൂന്ന് ശരിയായ റെസ്പോൺസുകളും വെറുതെ നഷ്ടപ്പെടും. ഡാറ്റയുടെ നാലിലൊന്ന് ഭാഗം മാത്രം തകരാറിലായതുകൊണ്ട് ഉപയോക്താവിന് ഒരു സ്പിന്നറും (spinner) തുടർന്ന് ഒരു ഫെയിലർ സ്ക്രീനും മാത്രമേ കാണാൻ കഴിയൂ.

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

ഒരു റെസ്പോൺസും പാഴാക്കപ്പെടുന്നില്ല. നിങ്ങൾക്ക് ലഭ്യമായവ കാണിക്കാനും പരാജയപ്പെട്ട ഭാഗം വേർതിരിക്കാനും സാധിക്കും. പരസ്പരബന്ധമില്ലാത്ത പ്രവർത്തനങ്ങൾ കൈകാര്യം ചെയ്യുമ്പോൾ—ഉദാഹരണത്തിന് ബൾക്ക് നോട്ടിഫിക്കേഷനുകൾ, തേർഡ് പാർട്ടി വെബ്ഹൂക്ക് ഡിസ്പാച്ചുകൾ, അല്ലെങ്കിൽ ഒന്നിലധികം CSV സ്ട്രീമുകളിൽ നിന്ന് റെക്കോർഡുകൾ ഇംപോർട്ട് ചെയ്യുക എന്നിവയിൽ—ഈ പാറ്റേൺ വളരെ പ്രധാനമാണ്. നിങ്ങൾക്ക് ഇപ്പോഴും സെൻട്രലൈസ്ഡ് എറർ ട്രാക്കിംഗ് ആവശ്യമാണ്, എങ്കിലും നിങ്ങളുടെ ആപ്ലിക്കേഷൻ തകരാതെ മുന്നോട്ട് പോകും.

ഒരു പ്രായോഗിക കാര്യം: allSettled മുഴുവൻ സെറ്റും തിരികെ നൽകുന്നു, അതിനാൽ റിസൾട്ടുകൾ പരിശോധിച്ച ശേഷം നിങ്ങളുടെ ഫീച്ചറിന് "പാർഷ്യൽ സക്സസ്" (partial success) എന്നാൽ എന്താണെന്ന് നിങ്ങൾ തീരുമാനിക്കേണ്ടതുണ്ട്. തിരികെ ലഭിക്കുന്ന അറേയെ മുഴുവൻ ശരിയായ ഡാറ്റയായി കാണരുത്. സ്റ്റേറ്റ് ലെയറിലേക്ക് (state layer) എന്തെങ്കിലും നൽകുന്നതിന് മുമ്പ് സ്റ്റാറ്റസ് ഫീൽഡുകൾ പരിശോധിക്കുക.

Top-Level await ഉപയോഗിക്കുക, Wrapper IIFE ഒഴിവാക്കുക

വർഷങ്ങളായി, ഒരു ഫയലിന്റെ റൂട്ടിൽ (root) എന്തെങ്കിലും await ചെയ്യണമെങ്കിൽ, നിങ്ങൾ അത് ഒരു 'immediately invoked async function'-ൽ ഉൾപ്പെടുത്തിയിരുന്നു:

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

ഇത് പ്രവർത്തിക്കും, പക്ഷേ ഇത് അനാവശ്യമായ കോഡ് (noise) ആണ്. ES modules-ൽ ലഭ്യമായ Top-level await ഉപയോഗിക്കുന്നതിലൂടെ ഈ അനാവശ്യമായ ബൊയിലർപ്ലേറ്റ് (boilerplate) ഒഴിവാക്കാം:

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

നിങ്ങളുടെ ആപ്ലിക്കേഷന്റെ എൻട്രി പോയിന്റിലോ (entry point), അല്ലെങ്കിൽ മറ്റെന്തെങ്കിലും പ്രവർത്തിക്കുന്നതിന് മുമ്പ് ഇനീഷ്യലൈസേഷൻ പൂർത്തിയാകേണ്ട കോൺഫിഗറേഷൻ മോഡ്യൂളുകളിലോ ഇത് ഉപയോഗിക്കാം. എൻവയോൺമെന്റ് ഫയലുകൾ ലോഡ് ചെയ്യുക, ഡാറ്റാബേസ് കണക്ഷൻ പൂൾ (connection pool) സ്ഥാപിക്കുക, അല്ലെങ്കിൽ റിമോട്ട് ഫീച്ചർ ഫ്ലാഗുകൾ (feature flags) എടുക്കുക എന്നിവ ഇതിന് അനുയോജ്യമാണ്. Top-level await മോഡ്യൂൾ ഗ്രാഫിന്റെ (module graph) എക്സിക്യൂഷൻ തടയുന്നതിനാൽ—ഈ ഫയൽ ഇംപോർട്ട് ചെയ്യുന്ന മറ്റ് ഫയലുകൾ നിങ്ങളുടെ പ്രോമിസ് പൂർത്തിയാകുന്നത് വരെ കാത്തുനിൽക്കും—നിങ്ങൾക്ക് ഉറപ്പുള്ള ഒരു സ്റ്റേറ്റ് ലഭിക്കുന്നു. നിങ്ങളുടെ കോഡ്ബേസിലെ ബാക്കി ഭാഗങ്ങൾക്ക് 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.