JavaScript च्या async/await सिंटॅक्समुळे आपल्याला 'callback hell' मधून सुटका मिळेल अशी अपेक्षा होती. त्याऐवजी, त्याने एक शांत पण अधिक घातक समस्या निर्माण केली: असा कोड जो दिसायला बरोबर वाटतो पण त्याचे वर्तन अनपेक्षित असते. तुम्ही फंक्शन बॉडीमध्ये await पाहता आणि असे गृहीत धरता की प्रत्येक ओळ व्यवस्थित थांबेल. पण अनेकदा तसे होत नाही. लूप्स वेगाने पुढे जातात. एका सिंगल रिक्वेस्ट फेल झाल्यामुळे संपूर्ण बॅच कोलमडून पडते. विनाकारण एन्ट्री फाइल्समध्ये (entry files) विचित्र async wrappers तयार होतात. जर तुम्ही यापैकी कोणत्याही समस्येचा सामना केला असेल, तर हे तीन पॅटर्न (patterns) त्या सुधारण्यास मदत करतील.
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 कीवर्ड प्रत्येक कॉलबॅकला एका प्रॉमिसमध्ये रूपांतरित करतो, ज्याकडे 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 कॉल्सची साखळी (chaining) तयार करणे जिथे पुढच्या रिक्वेस्टला मागील रिस्पॉन्सचा डेटा लागतो. जर तुम्हाला खरोखर पॅरलल एक्झिक्यूशन (parallel execution) हवे असेल, तर forEach सोबत प्रयोग करू नका. त्याऐवजी स्पष्टपणे Promise.all वापरा, जेणेकरून पुढच्या डेव्हलपरला तुमचा हेतू समजेल. पण सिंक्रोनस (synchronous) वर्तनाची अपेक्षा ठेवून await आणि forEach कधीही एकत्र वापरू नका. तसे होणार नाही.
जेव्हा 'शून्य' हा पर्याय असू शकत नाही, तेव्हा Promise.allSettled वापरा
Promise.all हे सिमेंटिकली (semantically) प्रामाणिक आहे. त्याला प्रॉमिसचा एक ॲरे (array) द्या आणि ते रिझल्ट्सचा ॲरे परत करते. पण अडचण अशी आहे की, एखादा प्रॉमिस रिजेक्ट (reject) झाला की संपूर्ण प्रक्रिया लगेच रिजेक्ट होते. इतर सर्व प्रलंबित (pending) प्रॉमिस आपापल्या पद्धतीने पूर्ण होतात, पण तुम्हाला त्यांचे रिझल्ट्स मिळत नाहीत. प्रोडक्शनमध्ये (production), हे 'ऑल-ओर-नथिंग' (all-or-nothing) वर्तन त्रासदायक ठरते.
कल्पना करा की तुमचे ॲप्लिकेशन डॅशबोर्डचे विगेट्स (widgets) चार स्वतंत्र सर्व्हिसेसमधून मिळवते: ट्रॅफिक ॲनालिटिक्स, रेव्हेन्यू डेटा, युजर फीडबॅक आणि सर्व्हर हेल्थ. जर रेव्हेन्यू API मध्ये थोडा वेळ टाईमआउट (timeout) झाला, तर 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);
}
});
कोणताही रिस्पॉन्स फेकला जात नाही. तुम्ही जे उपलब्ध आहे ते रेंडर (render) करू शकता आणि फक्त फेल्युअरला वेगळे करू शकता. जेव्हा तुम्ही असंबंधित ऑपरेशन्स हाताळत असता—उदा. बल्क नोटिफिकेशन्स, थर्ड-पार्टी वेबहुक डिस्पॅचेस, किंवा अनेक CSV स्ट्रीम्समधून रेकॉर्ड्स इम्पोर्ट करणे—तेव्हा हा पॅटर्न खूप महत्त्वाचा ठरतो. तुम्हाला अजूनही सेंट्रलाइज्ड एरर ट्रॅकिंगची गरज असेल, पण तुमचे ॲप्लिकेशन स्थिर राहील.
एक व्यावहारिक टीप: allSettled पूर्ण सेट परत करते, त्यामुळे तुम्हाला रिझल्ट्स तपासावे लागतील आणि तुमच्या फीचरसाठी "पार्शियल सक्सेस" (partial success) म्हणजे काय, हे ठरवावे लागेल. मिळालेला ॲरे सर्व डेटा 'यशस्वी' आहे असे मानून वापरू नका. तुमच्या स्टेट लेयरमध्ये (state layer) काहीही पुश करण्यापूर्वी त्यातील status फील्ड्स तपासा.
Top-Level await घोषित करा आणि Wrapper IIFE बंद करा
अनेक वर्षांपासून, जर तुम्हाला फाईलच्या रूटवर (root) काहीतरी await करायचे असेल, तर तुम्हाला ते 'immediately invoked async function' मध्ये रॅप (wrap) करावे लागायचे:
(async () => {
const config = await loadConfig();
startServer(config);
})();
हे काम करते, पण ते अनावश्यक कोड (noise) निर्माण करते. ES modules मध्ये उपलब्ध असलेल्या 'Top-level await' मुळे तुम्ही हा अनावश्यक बॉयलरप्लेट (boilerplate) कोड काढून टाकू शकता:
const config = await loadConfig();
startServer(config);
तुमच्या ॲप्लिकेशनच्या एन्ट्री पॉइंटवर (entry point) किंवा समर्पित कॉन्फिगरेशन मॉड्यूल्समध्ये याचा वापर करा, जिथे इतर काहीही सुरू होण्यापूर्वी इनिशियलायझेशन (initialization) पूर्ण होणे आवश्यक आहे. एन्व्हायरमेंट फाइल्स लोड करणे, डेटाबेस कनेक्शन पूल प्रस्थापित करणे किंवा रिमोट फीचर फ्लॅग्स मिळवणे यासाठी हे उत्तम आहे. कारण top-level await मॉड्यूल ग्राफचे एक्झिक्यूशन थांबवते—म्हणजेच या फाईलला इम्पोर्ट करणाऱ्या इतर फाइल्स तुमच्या प्रॉमिसच्या रिझोल्यूशनची वाट पाहतील—त्यामुळे तुम्हाला खात्रीशीर स्टेट (guaranteed state) मिळते. तुमच्या कोडबेसमधील इतर भाग db इम्पोर्ट करू शकतात आणि त्यांना माहित असेल की कनेक्शन आधीच सुरू झाले आहे.
यात दोन अडचणी आहेत. पहिले म्हणजे, तुमच्या रनटाइम किंवा बंडलरने (bundler) ES modules ला सपोर्ट करणे आवश्यक आहे. Node.js मध्ये, याचा अर्थ एकतर .mjs एक्स्टेंशन वापरणे किंवा तुमच्या package.json मध्ये "type": "module" सेट करणे असा होतो. दुसरे म्हणजे, मॉड्यूल लेव्हलवर प्रतीक्षा केल्यामुळे प्रत्येक इम्पॉर्टरला (importer) विलंब होतो, त्यामुळे await केलेले काम मर्यादित आणि केंद्रित ठेवा. वारंवार इम्पॉर्ट केल्या जाणाऱ्या युटिलिटी फाईलच्या सुरुवातीला जड सिक्वेन्शिअल फेचेस (sequential fetches) असल्यास तुमच्या संपूर्ण ॲप्लिकेशनचा 'कोल्ड स्टार्ट' (cold start) मंदावेल. टॉप-लेव्हल await फक्त अशा महत्त्वाच्या बूटस्ट्रॅप (bootstrap) कामांसाठी वापरा ज्यावर इतर मॉड्यूल्स खरोखर अवलंबून आहेत.
जेव्हा तुम्ही हे पॅटर्न स्वीकारता, तेव्हा प्रत्यक्षात काय बदलते
प्रेडिक्टेबिलिटी (Predictability) हा पहिला फायदा आहे. जेव्हा तुम्ही for...of लूप वाचता, तेव्हा त्याखालील ब्लॉक नेमका कधी संपेल हे तुम्हाला अचूक समजते. पडद्यामागे धावणारे कोणतेही 'घोस्ट प्रॉमिस' (ghost promises) नसतात किंवा तुमच्या एरर हँडलर्सपासून (error handlers) वेगळे होणारे foreach कॉलबॅक नसतात. तुमचा कंट्रोल फ्लो (control flow) स्क्रीनवरील कोडच्या रचनेशी सुसंगत असतो.
त्यानंतर येतो रेझिलियन्स (Resilience). Promise.allSettled तुम्हाला प्रत्येक बाह्य सिस्टम (external system) परिपूर्ण राहील अशी आशा करण्याऐवजी, अंशतः अपयशाचा (partial failure) विचार करण्यास भाग पाडते. प्रोडक्शन सॉफ्टवेअर हे बायनरी (binary) नसते. काही एंडपॉइंट्स (endpoints) नीट चालणार नाहीत. काही फाईल रीड्समध्ये परमिशन एरर्स (permission errors) येतील. विखुरलेल्या अपयशांच्या (scattered failures) वास्तवाचा विचार करून डिझाइन केल्यामुळे, वैध डेटा दुर्लक्षित न करता तुमचे ॲप्लिकेशन स्थिर राहते.
स्पष्टता (Clarity) या सर्वांना एकत्र बांधते. for...of वाचताना एखाद्या साध्या इंग्रजी प्रगतीसारखे वाटते. allSettled त्याच्या नावावरूनच त्याचा उद्देश स्पष्ट करते. टॉप-लेव्हल await मुळे गुंतागुंतीचे IIFE रॅपर्स (wrappers) काढून टाकले जातात, ज्यामुळे तुमचे एन्ट्री फाइल्स (entry files) सिंटॅक्टिक ॲक्रोबॅटिक्सऐवजी (syntactic acrobatics) थेट बिझनेस लॉजिकने सुरू होतात. या फाईलवर काम करणारा पुढचा इंजिनिअर—मग तो सहा महिन्यांनंतर तुम्ही स्वतः असाल किंवा डेडलाईनवर असलेला तुमचा सहकारी—तुमचे आभार मानून जाईल.
महत्त्वाचा निष्कर्ष
async/await ला अस्तित्वात असलेल्या कोडवर फक्त वरवर टाकलेले एखादे 'ग्लोबल फिक्स' (global fix) समजू नका. तुमच्या सध्याच्या प्रोजेक्ट्समध्ये या तीन विशिष्ट अँटी-पॅटर्नसाठी (anti-patterns) ऑडिट करा. forEach ब्लॉक्समधील await शोधा आणि त्याऐवजी for...of किंवा जाणीवपूर्वक वापरलेले Promise.all वापरा. बाह्य सेवांशी (external services) संवाद साधणाऱ्या प्रत्येक Promise.all चे पुनरावलोकन करा आणि विचार करा की एका अपयशाने संपूर्ण ऑपरेशन खरोखरच कोलमडले पाहिजे का; जर नसेल, तर Promise.allSettled वर स्विच करा आणि मिश्र निकालांचे (mixed results) व्यवस्थापन करा. शेवटी, तुमच्या ES module एन्ट्री पॉइंट्समधून async IIFEs काढून टाका आणि तुमच्या बूटस्ट्रॅप सिक्वेन्ससाठी (bootstrap sequence) थेट टॉप-लेव्हल await चा वापर करा. हे छोटे तांत्रिक बदल आहेत, परंतु एकत्रितपणे ते नाजूक असणारे असिंक्रोनस स्क्रिप्ट्स (asynchronous scripts) अशा कोडमध्ये रूपांतरित करतात ज्यावर तुम्ही खरोखर विश्वास ठेवू शकता.
