JavaScript ની async/await સિન્ટેક્સ આપણને callback hell થી બચાવવા માટે હતી. તેના બદલે, તેણે એક શાંત અને વધુ કપટી સમસ્યા રજૂ કરી: એવો કોડ જે સાચો લાગે છે પણ અનિશ્ચિત રીતે વર્તે છે. તમે ફંક્શન બોડીમાં 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 દરેક એલિમેન્ટ માટે કોલબેકને તરત જ એક્ઝિક્યુટ કરે છે. તે દરેક ઇટરેશનની અંદર રહેલા promise ની રાહ જોતું નથી. async કીવર્ડ દરેક કોલબેકને promise માં ફેરવે છે જેને forEach તરત જ અવગણે છે. તમારું લૂપ માઇક્રોસેકન્ડ્સમાં પૂરું થઈ જાય છે; નેટવર્ક રિક્વેસ્ટ્સ પોતાની રીતે આગળ વધી જાય છે. જો તમારે ભૂલો (errors) ક્રમમાં હેન્ડલ કરવી હોય, અથવા જો તમારે ખાતરી કરવી હોય કે એક રિક્વેસ્ટ બીજી શરૂ થાય તે પહેલા પૂરી થાય, તો આ પેટર્ન બંને ગેરંટીઓને શાંતિથી તોડી નાખે છે.

તેને 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 ને માનવા માટે એક સમયે એક ફાઇલ અપલોડ કરવી, ચોક્કસ ક્રમમાં ડેટાબેઝ રો (rows) લખવા, અથવા API કોલ્સને ચેઈન કરવા જ્યાં આગલી રિક્વેસ્ટને અગાઉના રિસ્પોન્સમાંથી ડેટાની જરૂર હોય. જો તમે ખરેખર પેરેલલ એક્ઝિક્યુશન (parallel execution) ઈચ્છતા હોવ, તો forEach સાથે ચેડાં ન કરો. સ્પષ્ટપણે Promise.all નો ઉપયોગ કરો જેથી આગામી ડેવલપરને તમારો ઈરાદો સમજાઈ શકે. પરંતુ સિંક્રનસ બિહેવિયર (synchronous behavior) ની અપેક્ષા રાખીને ક્યારેય await અને forEach ને મિક્સ ન કરો. તે થશે નહીં.

જ્યારે શૂન્ય (Zero) જવાબ ન હોઈ શકે ત્યારે Promise.allSettled નો ઉપયોગ કરો

Promise.all અર્થપૂર્ણ રીતે પ્રમાણિક છે. તેને promises નો એરે આપો, અને તે પરિણામોનો એરે પરત કરે છે. પણ પકડ એ છે કે જે ક્ષણે કોઈ એક promise રિજેક્ટ થાય છે, તે આખું કામ તરત જ રિજેક્ટ થઈ જાય છે. બાકીના તમામ પેન્ડિંગ promises ને પોતાની રીતે પૂરું થવા માટે છોડી દેવામાં આવે છે, પરંતુ તમે તેમના પરિણામો મેળવી શકતા નથી. પ્રોડક્શનમાં, આ "બધું અથવા કંઈ નહીં" (all-or-nothing) વાળું વર્તન નુકસાનકારક છે.

કલ્પના કરો કે તમારું એપ્લિકેશન ડેશબોર્ડના વિજેટ્સ (widgets) ચાર સ્વતંત્ર સેવાઓમાંથી મેળવે છે: ટ્રાફિક એનાલિટિક્સ, રેવન્યુ ડેટા, યુઝર ફીડબેક અને સર્વર હેલ્થ. રેવન્યુ API માં થોડો ટાઈમઆઉટ થાય છે. Promise.all હેઠળ, તમારું આખું ડેશબોર્ડ એરર આપે છે. બાકીના ત્રણ હેલ્ધી રિસ્પોન્સ પણ ખોવાઈ જાય છે. યુઝરને સ્પિનર દેખાય છે, અને પછી ફેઈલ્યોર સ્ક્રીન દેખાય છે, કારણ કે ડેટાનો માત્ર એક ચોથો ભાગ ખોટો હતો.

Promise.allSettled તમને વધુ સમજદારીભર્યું પરિણામ આપે છે. તે દરેક promise પૂરી થાય ત્યાં સુધી રાહ જુએ છે, પછી ભલે તે સફળ થાય કે નિષ્ફળ. રિઝોલ્વ થયેલ વેલ્યુ એ દરેક પરિણામનું વર્ણન કરતા ઓબ્જેક્ટ્સનો એરે છે:

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 ને દૂર કરો

વર્ષોથી, જો તમારે ફાઇલના રૂટ પર કંઈક 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 મોડ્યુલ ગ્રાફના એક્ઝિક્યુશનને બ્લોક કરે છે—આ ફાઇલ ઇમ્પોર્ટ કરતી અન્ય ફાઇલો તમારા promise રિઝોલ્વ થાય તેની રાહ જોશે—તમને ગેરંટીડ સ્ટેટ મળે છે. તમારા કોડબેઝના બાકીના ભાગ db ને ઇમ્પોર્ટ કરી શકે છે અને જાણી શકે છે કે કનેક્શન પહેલેથી જ લાઈવ છે.

અહીં બે અવરોધો છે. પ્રથમ, તમારા runtime અથવા bundler એ ES modules ને સપોર્ટ કરવા જ જોઈએ. Node.js માં, તેનો અર્થ એ છે કે કાં તો .mjs એક્સટેન્શનનો ઉપયોગ કરવો અથવા તમારા package.json માં "type": "module" સેટ કરવું. બીજું, મોડ્યુલ લેવલ પર રાહ જોવાથી દરેક ઇમ્પોર્ટર (importer) મોડું થાય છે, તેથી awaited કામને મર્યાદિત (focused) રાખો. વારંવાર ઇમ્પોર્ટ થતી યુટિલિટી ફાઇલના ટોચ પર ભારે સિક્વન્શિયલ ફેચ (sequential fetches) કરવાથી તમારા આખા એપ્લિકેશનનો કોલ્ડ સ્ટાર્ટ (cold start) ધીમો પડી જશે. top-level await ને ફક્ત એવા જ બુટસ્ટ્રેપ કાર્યો માટે રાખો જેના પર અન્ય મોડ્યુલ્સ ખરેખર નિર્ભર હોય.

જ્યારે તમે આ પેટર્ન અપનાવો છો ત્યારે ખરેખર શું બદલાય છે

અનુમાનિતતા (Predictability) એ પહેલો ફાયદો છે. જ્યારે તમે for...of લૂપ વાંચો છો, ત્યારે તમને ચોક્કસ ખબર હોય છે કે તેની નીચેનો બ્લોક ક્યારે પૂર્ણ થશે. પડદા પાછળ દોડતા કોઈ ghost promises નથી, કે તમારા error handlers થી અલગ થતા foreach callbacks નથી. તમારો કંટ્રોલ ફ્લો (control flow) સ્ક્રીન પરના કોડના આકાર સાથે સુસંગત રહે છે.

ત્યારબાદ આવે છે સ્થિતિસ્થાપકતા (Resilience). Promise.allSettled તમને દરેક એક્સટર્નલ સિસ્ટમ પરફેક્ટ રહેશે તેવી આશા રાખવાને બદલે આંશિક નિષ્ફળતા (partial failure) વિશે વિચારવા માટે મજબૂર કરે છે. પ્રોડક્શન સોફ્ટવેર બાઈનરી (binary) નથી હોતું. કેટલાક એન્ડપોઇન્ટ્સ (endpoints) નિષ્ફળ જશે. કેટલાક ફાઇલ રીડમાં પરમિશન એરર આવશે. વિખરાયેલી નિષ્ફળતાઓની વાસ્તવિકતા માટે ડિઝાઇન કરવાથી તમારી એપ્લિકેશન સ્થિર રહે છે અને સાચો ડેટા અવગણવામાં આવતો નથી.

સ્પષ્ટતા (Clarity) આ બધું એકસાથે જોડે છે. for...of વાંચવામાં સાદી અંગ્રેજી પ્રગતિ જેવું લાગે છે. allSettled તેના નામ દ્વારા જ તેનો હેતુ સ્પષ્ટ કરે છે. Top-level await રહસ્યમય IIFE વ્રેપર્સને દૂર કરે છે જેથી તમારી એન્ટ્રી ફાઇલો સિન્ટેક્ટિક જટિલતાઓ (syntactic acrobatics) ને બદલે બિઝનેસ લોજિકથી શરૂ થાય. જે આગામી એન્જિનિયર આ ફાઇલને સ્પર્શશે—પછી તે છ મહિના પછી તમે હોવ અથવા ડેડલાઇન પર કામ કરતો તમારો સાથીદાર હોય—તે તમારો આભાર માનશે.

મુખ્ય વાત

async/await ને કોઈ એવા ગ્લોબલ ફિક્સ તરીકે ન જુઓ જેને તમે હાલના કોડ પર છાંટી શકો. તમારા વર્તમાન પ્રોજેક્ટ્સમાં આ ત્રણ ચોક્કસ એન્ટી-પેટર્ન્સ (anti-patterns) માટે ઓડિટ કરો. forEach બ્લોક્સની અંદર await શોધો અને તેને for...of અથવા ઇરાદાપૂર્વક Promise.all સાથે બદલો. એક્સટર્નલ સર્વિસ સાથે વાત કરતા દરેક Promise.all ની સમીક્ષા કરો અને પૂછો કે શું એક જ નિષ્ફળતા ખરેખર સમગ્ર કામગીરીને તોડી નાખવી જોઈએ; જો નહીં, તો Promise.allSettled પર સ્વિચ કરો અને મિશ્ર પરિણામોને હેન્ડલ કરો. અંતે, તમારા ES module એન્ટ્રી પોઈન્ટ્સમાંથી async IIFEs ને દૂર કરો અને top-level await ને તમારી બુટસ્ટ્રેપ સિક્વન્સ સીધી રીતે હેન્ડલ કરવા દો. આ નાના મિકેનિકલ ફેરફારો છે, પરંતુ સાથે મળીને તેઓ નાજુક અસિંક્રોનસ સ્ક્રિપ્ટ્સને એવા કોડમાં ફેરવી દે છે જેના પર તમે ખરેખર વિશ્વાસ કરી શકો છો.