JavaScript کا async/await سنٹیکس ہمیں callback hell سے بچانے کے لیے بنایا گیا تھا۔ اس کے بجائے، اس نے ایک خاموش اور زیادہ خطرناک مسئلہ پیدا کر دیا: ایسا کوڈ جو دیکھنے میں درست لگتا ہے لیکن غیر متوقع طریقے سے کام کرتا ہے۔ آپ فنکشن باڈی میں await کو موجود دیکھتے ہیں اور فرض کر لیتے ہیں کہ ہر چیز لائن بہ لائن شائستگی سے رک رہی ہے۔ اکثر ایسا نہیں ہوتا۔ لوپس (loops) تیزی سے آگے نکل جاتے ہیں۔ ایک بھی ناکام ریکویسٹ کی وجہ سے پورے بیچز (batches) تباہ ہو جاتے ہیں۔ انٹری فائلز بلاوجہ عجیب و غریب 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 کی ورڈ ہر کال بیک کو ایک پرومس میں بدل دیتا ہے جسے 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!' صرف تب پرنٹ ہوتا ہے جب سب کچھ مکمل ہو جائے۔
for...of کا استعمال تب کریں جب ترتیب (sequence) اہم ہو—جیسے ریٹ لمٹس (rate limits) کا خیال رکھتے ہوئے فائلیں ایک ایک کر کے اپ لوڈ کرنا، ڈیٹا بیس کی روز (rows) کو ایک مخصوص ترتیب میں لکھنا، یا API کالز کو چین کرنا جہاں اگلی ریکویسٹ کو پچھلے رسپانس سے ڈیٹا کی ضرورت ہو۔ اگر آپ واقعی متوازی عمل (parallel execution) چاہتے ہیں، تو forEach کے ساتھ الجھنے کے بجائے واضح طور پر Promise.all کا استعمال کریں۔ لیکن کبھی بھی سنکرونس (synchronous) رویے کی توقع میں await اور forEach کو مکس نہ کریں۔ ایسا نہیں ہوگا۔
Promise.allSettled کا استعمال کریں جب 'صفر' جواب نہ ہو سکے
Promise.all معنوی طور پر ایماندار ہے۔ اسے پرومیسز کا ایک ایرے (array) دیں، اور یہ نتائج کا ایک ایرے واپس کرتا ہے۔ مسئلہ یہ ہے کہ جیسے ہی کوئی ایک پرومیس ریجیکٹ (reject) ہوتا ہے، پورا عمل فوری طور پر ریجیکٹ ہو جاتا ہے۔ باقی تمام زیر التواء (pending) پرومیسز اپنے آپ مکمل ہونے کے لیے چھوڑ دی جاتی ہیں، لیکن آپ ان کے نتائج تک رسائی کھو دیتے ہیں۔ پروڈکشن میں، یہ "سب کچھ یا کچھ بھی نہیں" والا رویہ نقصان دہ ثابت ہوتا ہے۔
تصور کریں کہ آپ کی ایپلی کیشن ایک ڈیش بورڈ کے ویجٹس (widgets) چار آزاد سروسز سے حاصل کرتی ہے: ٹریفک اینالیٹکس، ریونیو ڈیٹا، یوزر فیڈ بیک، اور سرور ہیلتھ۔ ریونیو API میں تھوڑی دیر کے لیے ٹائم آؤٹ (timeout) ہو جاتا ہے۔ Promise.all کے تحت، آپ کا پورا ڈیش بورڈ ایرر (error) دکھاتا ہے۔ تین درست رسپانسز ضائع ہو جاتے ہیں۔ صارف کو صرف ایک سپنر (spinner) نظر آتا ہے، اور پھر فیل ہونے والی اسکرین، کیونکہ ڈیٹا کا صرف ایک چوتھائی حصہ غلط نکلا۔
Promise.allSettled آپ کو ایک بہتر طریقہ فراہم کرتا ہے۔ یہ تب تک انتظار کرتا ہے جب تک کہ ہر ایک پرومیس مکمل نہ ہو جائے، چاہے وہ کامیاب ہو یا ناکام۔ اس کی ریزولوڈ ویلیو (resolved value) آبجیکٹس کا ایک ایسا ایرے ہے جو ہر نتیجے کی وضاحت کرتا ہے:
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) پر کسی چیز کا انتظار کرنا چاہتے تھے، تو آپ اسے ایک فوری طور پر کال ہونے والے (immediately invoked) async فنکشن میں لپیٹ دیتے تھے:
(async () => {
const config = await loadConfig();
startServer(config);
})();
یہ کام تو کرتا ہے، لیکن یہ غیر ضروری کوڈ (noise) ہے۔ ES ماڈیولز میں موجود نیٹیو (native) Top-level await آپ کو اس اضافی بوائلر پلیٹ (boilerplate) سے نجات دلاتا ہے:
const config = await loadConfig();
startServer(config);
اسے اپنی ایپلی کیشن کے انٹری پوائنٹ (entry point) پر یا مخصوص کنفیگریشن ماڈیولز میں استعمال کریں جہاں کسی بھی دوسری چیز کے چلنے سے پہلے انیشلائزیشن (initialization) کا مکمل ہونا ضروری ہو۔ انوائرمنٹ فائلز لوڈ کرنا، ڈیٹا بیس کنکشن پول قائم کرنا، یا ریموٹ فیچر فلیگز حاصل کرنا، یہ سب اس کے لیے موزوں ہیں۔ چونکہ top-level await ماڈیول گراف کے عمل کو روک دیتا ہے—یعنی اس فائل کو امپورٹ کرنے والی دوسری فائلیں آپ کے پرومس کے حل ہونے کا انتظار کریں گی—اس لیے آپ کو ایک یقینی اسٹیٹ (guaranteed state) ملتی ہے۔ آپ کے کوڈ بیس کا باقی حصہ db کو امپورٹ کر سکتا ہے اور جان سکتا ہے کہ کنکشن پہلے ہی فعال ہو چکا ہے۔
اس میں دو اہم پہلو ہیں۔ پہلا یہ کہ آپ کے runtime یا bundler کو ES modules کی سپورٹ کرنی چاہیے۔ Node.js میں، اس کا مطلب ہے یا تو .mjs extensions کا استعمال کرنا یا اپنے package.json میں "type": "module" سیٹ کرنا۔ دوسرا یہ کہ چونکہ module کی سطح پر انتظار کرنے سے ہر importer میں تاخیر ہوتی ہے، اس لیے awaited کام کو محدود (focused) رکھیں۔ کسی ایسی utility فائل کے آغاز میں جو بار بار امپورٹ کی جاتی ہے، بھاری sequential fetches آپ کی پوری ایپلی کیشن کے cold start کو سست کر دیں گے۔ top-level await کو صرف ان حقیقی bootstrap کاموں کے لیے مخصوص رکھیں جن پر دوسرے modules کا حقیقی انحصار ہو۔
ان پیٹرنز کو اپنانے سے اصل میں کیا تبدیلیاں آتی ہیں
پہلا فائدہ پیش گوئی (Predictability) ہے۔ جب آپ for...of loop پڑھتے ہیں، تو آپ کو بالکل معلوم ہوتا ہے کہ اس کے نیچے والا بلاک کب ختم ہوگا۔ پس منظر میں کوئی "ghost promises" مقابلہ نہیں کر رہے ہوں گے، اور نہ ہی کوئی foreach callbacks آپ کے error handlers سے الگ ہو رہے ہوں گے۔ آپ کا control flow اسکرین پر موجود کوڈ کی شکل سے مطابقت رکھتا ہے۔
اگلا فائدہ استحکام (Resilience) ہے۔ Promise.allSettled آپ کو اس امید کے بجائے کہ ہر بیرونی سسٹم مکمل طور پر درست رہے گا، جزوی ناکامی (partial failure) کے بارے میں سوچنے پر مجبور کرتا ہے۔ پروڈکشن سافٹ ویئر بائنری (binary) نہیں ہوتا۔ کچھ endpoints میں خرابی آئے گی۔ کچھ فائل ریڈز میں permission errors کا سامنا کرنا پڑے گا۔ بکھری ہوئی ناکامیوں کی حقیقت کو مدنظر رکھ کر ڈیزائن کرنا آپ کی ایپلی کیشن کو مستحکم رکھتا ہے اور جائز ڈیٹا کو نظر انداز ہونے سے بچاتا ہے۔
وضاحت (Clarity) ان سب کو یکجا کرتی ہے۔ for...of بالکل سادہ انگریزی ترتیب کی طرح پڑھا جاتا ہے۔ allSettled اپنے نام سے ہی اپنا مقصد واضح کر دیتا ہے۔ Top-level await پیچیدہ IIFE wrappers کو ختم کر دیتا ہے تاکہ آپ کی entry files syntactic acrobatics کے بجائے براہ راست business logic سے شروع ہوں۔ اگلا انجینئر جو اس فائل کو چھوئے گا—چاہے وہ چھ ماہ بعد آپ خود ہوں یا ڈیڈ لائن کے قریب کوئی ساتھی—وہ آپ کا شکر گزار ہوگا۔
ایک عملی سبق
async/await کو کسی ایسے عالمی حل (global fix) کے طور پر نہ لیں جسے آپ موجودہ کوڈ پر چھڑک دیں۔ اپنے موجودہ پروجیکٹس کا ان تین مخصوص anti-patterns کے لیے آڈٹ کریں۔ forEach بلاکس کے اندر await تلاش کریں اور انہیں for...of یا کسی بامقصد Promise.all سے بدل دیں۔ ہر اس Promise.all کا جائزہ لیں جو بیرونی سروسز سے رابطہ کرتا ہے اور خود سے پوچھیں کہ کیا ایک ناکامی واقعی پورے آپریشن کو تباہ کر دینی چاہیے؛ اگر نہیں، تو Promise.allSettled پر منتقل ہو جائیں اور ملے جلے نتائج کو سنبھالیں۔ آخر میں، اپنے ES module entry points سے async IIFEs کو نکال دیں اور top-level await کو اپنی bootstrap sequence براہ راست سنبھالنے دیں۔ یہ چھوٹے میکانیکی تبدیلیاں ہیں، لیکن مل کر یہ کمزور asynchronous اسکرپٹس کو ایسے کوڈ میں بدل دیتی ہیں جس پر آپ واقعی بھروسہ کر سکتے ہیں۔
