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 కీవర్డ్ ప్రతి కాల్బ్యాక్ను ఒక ప్రామిస్గా మారుస్తుంది, కానీ 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 ఉపయోగించండి—ఉదాహరణకు రేట్ లిమిట్స్ను గౌరవించడానికి ఫైల్లను ఒక్కొక్కటిగా అప్లోడ్ చేయడం, డేటాబేస్ రోస్ను ఒక నిర్దిష్ట క్రమంలో రాయడం, లేదా ఒక API కాల్ నుండి వచ్చిన డేటా తదుపరి API కాల్కు అవసరమైనప్పుడు వాటిని చైన్ చేయడం. ఒకవేళ మీరు పారలల్ ఎగ్జిక్యూషన్ (parallel execution) కోరుకుంటే, forEach తో ఇబ్బంది పడకండి. మీ ఉద్దేశ్యం తదుపరి డెవలపర్కు స్పష్టంగా తెలియడానికి నేరుగా Promise.all ఉపయోగించండి. కానీ సింక్రోనస్ (synchronous) ప్రవర్తన కోసం await మరియు forEachలను ఎప్పుడూ కలపకండి. అది జరగదు.
సున్నా (Zero) సమాధానం కానప్పుడు Promise.allSettled ఉపయోగించండి
Promise.all అనేది అర్థవంతంగా పనిచేస్తుంది. దీనికి ప్రామిస్ల అర్రే (array) ఇస్తే, అది ఫలితాల అర్రేను తిరిగి ఇస్తుంది. అయితే ఇక్కడే ఒక చిక్కు ఉంది: ఏదైనా ఒక ప్రామిస్ రిజెక్ట్ (reject) అయిన వెంటనే, మొత్తం ప్రక్రియ వెంటనే రిజెక్ట్ అవుతుంది. మిగిలిన పెండింగ్ ప్రామిస్లు తమకు తాముగా పూర్తి కావడానికి వదిలేయబడతాయి, కానీ మీరు వాటి ఫలితాలను పొందలేరు. ప్రొడక్షన్ (production) వాతావరణంలో, ఈ "అన్నీ లేదా ఏమీ లేదు" (all-or-nothing) ప్రవర్తన ఇబ్బంది కలిగిస్తుంది.
మీ అప్లికేషన్ ఒక డాష్బోర్డ్ విడ్జెట్లను నాలుగు స్వతంత్ర సర్వీసుల నుండి పొందుతోందని ఊహించుకోండి: ట్రాఫిక్ అనలిటిక్స్, రెవెన్యూ డేటా, యూజర్ ఫీడ్బ్యాక్ మరియు సర్వర్ హెల్త్. రెవెన్యూ 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);
}
});
ఏ రెస్పాన్స్ కూడా వృథా కాదు. మీకు అందుబాటులో ఉన్న డేటాను మీరు ప్రదర్శించవచ్చు మరియు ఫెయిల్యూర్ అయిన దానిని విడిగా గుర్తించవచ్చు. సంబంధం లేని ఆపరేషన్లతో—ఉదాహరణకు బల్క్ నోటిఫికేషన్లు, థర్డ్-పార్టీ వెబ్హుక్ డిస్పాచ్లు లేదా బహుళ CSV స్ట్రీమ్ల నుండి రికార్డులను ఇంపోర్ట్ చేయడం వంటి పనుల సమయంలో ఈ ప్యాటర్న్ చాలా ముఖ్యం. మీకు సెంట్రలైజ్డ్ ఎర్రర్ ట్రాకింగ్ అవసరమే అయినప్పటికీ, మీ అప్లికేషన్ స్థిరంగా ఉంటుంది.
ఒక ముఖ్యమైన విషయం: allSettled పూర్తి సెట్ను తిరిగి ఇస్తుంది, కాబట్టి మీరు ఫలితాలను పరిశీలించి, మీ ఫీచర్కు "పాక్షిక విజయం" (partial success) అంటే ఏమిటో నిర్ణయించుకోవాలి. తిరిగి వచ్చిన అర్రేను అంతా సక్సెస్ అయిన డేటాగా భావించకండి. మీ స్టేట్ లేయర్లోకి (state layer) ఏదైనా పంపే ముందు ఆ స్టేటస్ ఫీల్డ్లను తప్పనిసరిగా తనిఖీ చేయండి.
Top-Level awaitని ఉపయోగించండి మరియు Wrapper IIFEని తొలగించండి
ఏళ్ల తరబడి, ఒక ఫైల్ యొక్క రూట్ (root) వద్ద ఏదైనా await చేయాలనుకుంటే, దానిని వెంటనే పిలవబడే (immediately invoked) async ఫంక్షన్లో చుట్టాలి (wrap చేయాలి):
(async () => {
const config = await loadConfig();
startServer(config);
})();
ఇది పనిచేస్తుంది, కానీ ఇది అనవసరమైన కోడ్ (noise). ES modulesలో నేటివ్గా ఉండే Top-level await, మీరు ఈ అనవసరమైన బాయిలర్ప్లేట్ (boilerplate) కోడ్ను వదిలేయడానికి అనుమతిస్తుంది:
const config = await loadConfig();
startServer(config);
మీ అప్లికేషన్ యొక్క ఎంట్రీ పాయింట్ వద్ద లేదా ఇనిషియలైజేషన్ (initialization) మిగిలిన పనుల కంటే ముందే పూర్తి కావాల్సిన డెడికేటెడ్ కాన్ఫిగరేషన్ మాడ్యూల్స్లో దీనిని ఉపయోగించండి. ఎన్విరాన్మెంట్ ఫైల్లను లోడ్ చేయడం, డేటాబేస్ కనెక్షన్ పూల్ను ఏర్పాటు చేయడం లేదా రిమోట్ ఫీచర్ ఫ్లాగ్లను పొందడం వంటి పనులకు ఇది సరిగ్గా సరిపోతుంది. Top-level await మాడ్యూల్ గ్రాఫ్ ఎగ్జిక్యూషన్ను బ్లాక్ చేస్తుంది—అంటే ఈ ఫైల్ను ఇంపోర్ట్ చేసే ఇతర ఫైల్లు మీ ప్రామిస్ రిజాల్వ్ అయ్యే వరకు వేచి ఉంటాయి—దీనివల్ల మీకు గ్యారెంటీగా ఉన్న స్టేట్ లభిస్తుంది. మీ కోడ్బేస్లోని మిగిలిన భాగాలు dbని ఇంపోర్ట్ చేసినప్పుడు, కనెక్షన్ ఇప్పటికే లైవ్లో ఉందని అవి తెలుసుకోగలవు.
ఇక్కడ రెండు ముఖ్యమైన విషయాలు ఉన్నాయి. మొదటిది, మీ runtime లేదా bundler తప్పనిసరిగా ES modules ను సపోర్ట్ చేయాలి. Node.jsలో, దీని అర్థం .mjs ఎక్స్టెన్షన్లను ఉపయోగించడం లేదా మీ package.jsonలో "type": "module" అని సెట్ చేయడం. రెండవది, మాడ్యూల్ స్థాయిలో వేచి ఉండటం (waiting) వల్ల ప్రతి ఇంపోర్టర్ ఆలస్యమవుతుంది కాబట్టి, మీరు చేసే await పనులను కేవలం అవసరమైన వాటికే పరిమితం చేయండి. తరచుగా ఇంపోర్ట్ చేయబడే ఒక utility ఫైల్ పైన భారీ సీక్వెన్షియల్ ఫెచ్లు (sequential fetches) ఉంటే, అది మీ మొత్తం అప్లికేషన్ కోల్డ్ స్టార్ట్ (cold start) వేగాన్ని తగ్గిస్తుంది. ఇతర మాడ్యూల్స్ నిజంగా ఆధారపడే అసలైన బూట్స్ట్రాప్ (bootstrap) పనుల కోసం మాత్రమే top-level await ను ఉపయోగించండి.
ఈ పద్ధతులను అనుసరించినప్పుడు వాస్తవంగా ఏమి మారుతుంది
Predictability అనేది మొదటి ప్రయోజనం. మీరు ఒక for...of లూప్ను చదివినప్పుడు, దాని కింద ఉన్న బ్లాక్ ఎప్పుడు పూర్తవుతుందో మీకు ఖచ్చితంగా తెలుస్తుంది. తెర వెనుక తెలియని ప్రామిసెస్ (ghost promises) నడుస్తూ ఉండవు, లేదా మీ ఎర్రర్ హ్యాండ్లర్ల నుండి foreach కాల్బ్యాక్లు విడిపోవు. మీ కంట్రోల్ ఫ్లో (control flow) స్క్రీన్పై ఉన్న కోడ్ రూపంతో సరిపోలుతుంది.
Resilience అనేది తదుపరి ప్రయోజనం. ప్రతి ఎక్స్టర్నల్ సిస్టమ్ పర్ఫెక్ట్గా ఉంటుందని ఆశించే బదులు, పాక్షిక వైఫల్యాల (partial failure) గురించి ఆలోచించేలా Promise.allSettled మిమ్మల్ని ప్రేరేపిస్తుంది. ప్రొడక్షన్ సాఫ్ట్వేర్ అనేది కేవలం 'అవును' లేదా 'కాదు' (binary) లాంటిది కాదు. కొన్ని ఎండ్పాయింట్లు సరిగ్గా పనిచేయకపోవచ్చు. కొన్ని ఫైల్ రీడ్లు పర్మిషన్ ఎర్రర్లను ఎదుర్కోవచ్చు. ఇలాంటి విడివిడి వైఫల్యాల వాస్తవికతను దృష్టిలో ఉంచుకుని డిజైన్ చేయడం వల్ల, సరైన డేటాను వదిలేయకుండానే మీ అప్లికేషన్ స్థిరంగా ఉంటుంది.
Clarity వీటన్నింటినీ ఏకం చేస్తుంది. for...of అనేది సాధారణ ఇంగ్లీష్ క్రమంలా చదవడానికి వీలుగా ఉంటుంది. allSettled తన పేరులోనే దాని ఉద్దేశ్యాన్ని తెలియజేస్తుంది. Top-level await వల్ల అర్థం కాని IIFE రాపర్ల అవసరం ఉండదు, తద్వారా మీ ఎంట్రీ ఫైల్స్ సంక్లిష్టమైన సింటాక్స్ (syntactic acrobatics) కు బదులుగా నేరుగా బిజినెస్ లాజిక్తో ప్రారంభమవుతాయి. ఆ ఫైల్ను తదుపరి ఇంజనీర్ తాకినప్పుడు—అది ఆరు నెలల తర్వాత మీరే కావచ్చు లేదా డెడ్లైన్లో ఉన్న మీ సహోద్యోగి కావచ్చు—వారు మీకు కృతజ్ఞతలు చెబుతారు.
ముఖ్యమైన సారాంశం
async/await ను ఉన్న కోడ్పై చల్లే ఒక గ్లోబల్ ఫిక్స్ (global fix) లాగా భావించకండి. ఈ మూడు నిర్దిష్ట యాంటీ-ప్యాటర్న్ల (anti-patterns) కోసం మీ ప్రస్తుత ప్రాజెక్ట్లను తనిఖీ చేయండి. forEach బ్లాక్ల లోపల await కోసం వెతకండి మరియు వాటిని for...of లేదా ఉద్దేశపూర్వకమైన Promise.allతో మార్చండి. ఎక్స్టర్నల్ సర్వీసులతో కనెక్ట్ అయ్యే ప్రతి Promise.allను సమీక్షించండి మరియు ఒకే ఒక వైఫల్యం మొత్తం ఆపరేషన్ను దెబ్బతీయాలా అని ఆలోచించండి; ఒకవేళ అవసరం లేకపోతే, Promise.allSettledకి మారి మిశ్రమ ఫలితాలను (mixed results) హ్యాండిల్ చేయండి. చివరగా, మీ ES module ఎంట్రీ పాయింట్ల నుండి async IIFEsలను తొలగించి, మీ బూట్స్ట్రాప్ సీక్వెన్స్ను నేరుగా top-level await ద్వారా నిర్వహించనివ్వండి. ఇవి చిన్న మెకానికల్ మార్పులే కావచ్చు, కానీ ఇవన్నీ కలిసి బలహీనమైన అసింక్రోనస్ స్క్రిప్ట్లను మీరు నిజంగా నమ్మగలిగే కోడ్గా మారుస్తాయి.
