JavaScript-এর async/await সিনট্যাক্স আমাদের callback hell থেকে বাঁচানোর কথা ছিল। পরিবর্তে, এটি একটি শান্ত অথচ আরও মারাত্মক সমস্যা তৈরি করেছে: এমন কোড যা দেখতে সঠিক মনে হলেও আচরণ করে অনিশ্চিতভাবে। আপনি ফাংশন বডিতে await দেখে ধরে নেন যে সবকিছু সুন্দরভাবে লাইন বাই লাইন থেমে যাবে। প্রায়শই তা হয় না। লুপগুলো দ্রুত এগিয়ে যায়। একটি মাত্র রিকোয়েস্ট ব্যর্থ হওয়ার কারণে পুরো ব্যাচটি ভেঙে পড়ে। কোনো কারণ ছাড়াই এন্ট্রি ফাইলগুলোতে কুৎসিত async wrapper তৈরি করতে হয়। আপনি যদি এগুলোর কোনোটির সম্মুখীন হয়ে থাকেন, তবে এই তিনটি প্যাটার্ন সেগুলো ঠিক করে দেবে।
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 দ্রুত উপেক্ষা করে। আপনার লুপটি মাইক্রোসেকেন্ডের মধ্যে শেষ হয়ে যায়; কিন্তু নেটওয়ার্ক রিকোয়েস্টগুলো তাদের নিজস্ব গতিতে চলতে থাকে। আপনার যদি ত্রুটিগুলো (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!' প্রিন্ট হবে।
যখন সিকোয়েন্স বা ক্রম গুরুত্বপূর্ণ—যেমন রেট লিমিট মেনে ফাইলগুলো একটির পর একটি আপলোড করা, নির্দিষ্ট ক্রমে ডাটাবেস রো (rows) লেখা, অথবা API কল চেইনিং করা যেখানে পরবর্তী রিকোয়েস্টের জন্য পূর্ববর্তী রেসপন্সের ডেটা প্রয়োজন—তখন for...of ব্যবহার করুন। আপনি যদি প্রকৃতপক্ষে প্যারালাল এক্সিকিউশন (parallel execution) চান, তবে forEach নিয়ে নাড়াচাড়া করবেন না। স্পষ্টভাবে Promise.all ব্যবহার করুন যাতে পরবর্তী ডেভেলপার আপনার উদ্দেশ্য বুঝতে পারেন। কিন্তু সিনক্রোনাস (synchronous) আচরণের প্রত্যাশায় কখনোই await এবং forEach একসাথে মিশিয়ে ফেলবেন না। এটি কাজ করবে না।
যখন শূন্য কোনো সমাধান হতে পারে না, তখন Promise.allSettled ব্যবহার করুন
Promise.all অর্থগতভাবে সৎ। এটি প্রমিজের একটি অ্যারে গ্রহণ করে এবং ফলাফলের একটি অ্যারে রিটার্ন করে। সমস্যা হলো, যখনই কোনো একটি প্রমিজ রিজেক্ট (reject) হয়, পুরো বিষয়টি তাৎক্ষণিকভাবে রিজেক্ট হয়ে যায়। অন্যান্য সমস্ত পেন্ডিং প্রমিজগুলো তাদের নিজস্ব গতিতে শেষ হওয়ার জন্য ফেলে রাখা হয়, কিন্তু আপনি তাদের ফলাফলের অ্যাক্সেস হারিয়ে ফেলেন। প্রোডাকশন এনভায়রনমেন্টে, এই "সব অথবা কিছুই না" (all-or-nothing) আচরণটি বেশ ক্ষতিকর।
কল্পনা করুন আপনার অ্যাপ্লিকেশনটি চারটি স্বতন্ত্র সার্ভিস থেকে একটি ড্যাশবোর্ডের উইজেট (widgets) সংগ্রহ করছে: ট্রাফিক অ্যানালিটিক্স, রেভিনিউ ডেটা, ইউজার ফিডব্যাক এবং সার্ভার হেলথ। রেভিনিউ API-তে একটি সাময়িক টাইমআউট হলো। Promise.all ব্যবহারের ফলে আপনার পুরো ড্যাশবোর্ডটি একটি এরর (error) দেখাবে। বাকি তিনটি সঠিক রেসপন্সও হারিয়ে যাবে। ব্যবহারকারী কেবল একটি স্পিনার এবং তারপর একটি ফেইলর স্ক্রিন দেখবেন, কারণ মাত্র এক চতুর্থাংশ ডেটা সঠিকভাবে কাজ করেনি।
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);
}
});
কোনো রেসপন্সই বাদ পড়ে না। আপনি যা সম্ভব তা রেন্ডার করতে পারেন এবং ব্যর্থতাটিকে আলাদা রাখতে পারেন। যখনই আপনি সম্পর্কহীন অপারেশন নিয়ে কাজ করছেন—যেমন বাল্ক নোটিফিকেশন, থার্ড-পার্টি ওয়েবহুক ডিসপ্যাচ, বা একাধিক CSV স্ট্রিম থেকে রেকর্ড ইমপোর্ট করা—তখন এই প্যাটার্নটি অত্যন্ত গুরুত্বপূর্ণ। আপনার এখনও সেন্ট্রালাইজড এরর ট্র্যাকিং প্রয়োজন হবে, তবে আপনার অ্যাপ্লিকেশনটি স্থিতিশীল থাকবে।
একটি ব্যবহারিক নোট: allSettled সম্পূর্ণ সেটটি রিটার্ন করে, তাই আপনাকে এখনও ফলাফলগুলো যাচাই করতে হবে এবং আপনার ফিচারের জন্য "আংশিক সাফল্য" (partial success) বলতে কী বোঝায় তা নির্ধারণ করতে হবে। রিটার্ন করা অ্যারেটিকে সব ডেটা সফল হিসেবে ধরে নেবেন না। আপনার স্টেট লেয়ারে (state layer) কিছু পাঠানোর আগে অবশ্যই স্ট্যাটাস ফিল্ডগুলো চেক করে নিন।
Top-Level await ঘোষণা করুন এবং Wrapper IIFE বাদ দিন
বছরের পর বছর ধরে, আপনি যদি কোনো ফাইলের রুট লেভেলে কিছু await করতে চাইতেন, তবে আপনাকে এটিকে একটি ইমিডিয়েটলি ইনভোকড (immediately invoked) async ফাংশনে মুড়িয়ে রাখতে হতো:
(async () => {
const config = await loadConfig();
startServer(config);
})();
এটি কাজ করে ঠিকই, কিন্তু এটি অপ্রয়োজনীয় কোড (noise) তৈরি করে। ES মডিউলগুলোতে নেটিভভাবে থাকা Top-level await আপনাকে এই অতিরিক্ত বয়েলারপ্লেট (boilerplate) কোড বাদ দিতে সাহায্য করে:
const config = await loadConfig();
startServer(config);
আপনার অ্যাপ্লিকেশনের এন্ট্রি পয়েন্টে অথবা ডেডিকেটেড কনফিগারেশন মডিউলগুলোতে এটি ব্যবহার করুন যেখানে অন্য কিছু চলার আগে ইনিশিয়ালাইজেশন সম্পন্ন হওয়া প্রয়োজন। এনভায়রনমেন্ট ফাইল লোড করা, ডাটাবেস কানেকশন পুল স্থাপন করা, বা রিমোট ফিচার ফ্ল্যাগ ফেচ করা—এসব ক্ষেত্রে এটি চমৎকারভাবে কাজ করে। যেহেতু top-level await মডিউল গ্রাফের এক্সিকিউশন ব্লক করে—অর্থাৎ অন্য ফাইলগুলো যখন এই ফাইলটিকে ইমপোর্ট করবে, তারা আপনার প্রমিজটি রেজলভ হওয়া পর্যন্ত অপেক্ষা করবে—তাই আপনি একটি নিশ্চিত স্টেট (state) পাবেন। আপনার কোডবেসের বাকি অংশ db ইমপোর্ট করতে পারবে এবং নিশ্চিত থাকতে পারবে যে কানেকশনটি ইতিমধ্যে চালু হয়ে গেছে।
এখানে দুটি বিষয় মাথায় রাখতে হবে। প্রথমত, আপনার runtime বা bundler-কে অবশ্যই ES modules সাপোর্ট করতে হবে। Node.js-এর ক্ষেত্রে এর অর্থ হলো হয় .mjs extension ব্যবহার করা অথবা আপনার package.json-এ "type": "module" সেট করা। দ্বিতীয়ত, মডিউল লেভেলে অপেক্ষা করার ফলে প্রতিটি importer-এর কাজ বিলম্বিত হয়, তাই awaited কাজগুলোকে সুনির্দিষ্ট বা ফোকাসড রাখুন। ঘনঘন ইম্পোর্ট করা কোনো utility ফাইলের একদম শুরুতে ভারী sequential fetch অপারেশন থাকলে তা আপনার পুরো অ্যাপ্লিকেশনের cold start ধীর করে দেবে। top-level await শুধুমাত্র সেইসব আসল bootstrap টাস্কের জন্য রাখুন যেগুলোর ওপর অন্যান্য মডিউল প্রকৃতপক্ষে নির্ভর করে।
এই প্যাটার্নগুলো গ্রহণ করলে আসলে কী পরিবর্তন আসে
Predictability (পূর্বাভাসযোগ্যতা) হলো এর প্রথম সুফল। যখন আপনি একটি for...of লুপ পড়েন, তখন আপনি ঠিকভাবে জানেন যে নিচের ব্লকটি কখন শেষ হবে। পর্দার আড়ালে কোনো 'ghost promises' দৌড়াবে না, কিংবা আপনার error handler থেকে কোনো foreach callback বিচ্ছিন্ন হয়ে যাবে না। আপনার control flow স্ক্রিনে থাকা কোডের গঠনের সাথে মিলে যাবে।
Resilience (স্থিতিস্থাপকতা) এরপর আসে। Promise.allSettled আপনাকে প্রতিটি এক্সটার্নাল সিস্টেম নিখুঁত থাকবে বলে আশা করার পরিবর্তে আংশিক ব্যর্থতা (partial failure) নিয়ে চিন্তা করতে বাধ্য করে। প্রোডাকশন সফটওয়্যার বাইনারি নয়। কিছু endpoint কাজ নাও করতে পারে। কিছু ফাইল রিড করার সময় permission error আসতে পারে। ছড়িয়ে ছিটিয়ে থাকা ব্যর্থতার বাস্তবতাকে মাথায় রেখে ডিজাইন করলে আপনার অ্যাপ্লিকেশনটি ভেঙে পড়বে না এবং কোনো বৈধ ডেটাকে এড়িয়ে যেতে হবে না।
Clarity (স্পষ্টতা) সবকিছুকে এক সুতোয় গেঁথে দেয়। for...of পড়তে সাধারণ ইংরেজি অগ্রগতির মতো মনে হয়। allSettled তার নামের মাধ্যমেই তার উদ্দেশ্য প্রকাশ করে। Top-level await রহস্যময় IIFE wrapper সরিয়ে ফেলে, ফলে আপনার entry file-গুলো সিনট্যাকটিক কারিকুরি (syntactic acrobatics) এর বদলে সরাসরি business logic দিয়ে শুরু হয়। পরবর্তী ইঞ্জিনিয়ার যিনি এই ফাইলটি নিয়ে কাজ করবেন—সেটি ছয় মাস পর আপনি নিজে হোন বা ডেডলাইনের চাপে থাকা কোনো সহকর্মী—তিনি আপনাকে ধন্যবাদ জানাবেন।
একটি বাস্তব শিক্ষা
async/await-কে এমন কোনো গ্লোবাল সমাধান হিসেবে নেবেন না যা আপনি বিদ্যমান কোডের ওপর ছড়িয়ে দেবেন। আপনার বর্তমান প্রজেক্টগুলোতে এই তিনটি নির্দিষ্ট anti-pattern আছে কি না তা পরীক্ষা (audit) করুন। forEach ব্লকের ভেতরে await খুঁজুন এবং সেগুলোকে for...of অথবা একটি সুপরিকল্পিত Promise.all দিয়ে প্রতিস্থাপন করুন। এক্সটার্নাল সার্ভিসের সাথে যোগাযোগ করে এমন প্রতিটি Promise.all রিভিউ করুন এবং নিজেকে প্রশ্ন করুন যে একটি মাত্র ব্যর্থতা কি সত্যিই পুরো অপারেশনটিকে ধ্বংস করে দেবে? যদি না হয়, তবে Promise.allSettled-এ সুইচ করুন এবং মিশ্র ফলাফলগুলো হ্যান্ডেল করুন। সবশেষে, আপনার ES module entry point থেকে async IIFE সরিয়ে ফেলুন এবং bootstrap sequence সরাসরি হ্যান্ডেল করার জন্য top-level await ব্যবহার করুন। এগুলো ছোট যান্ত্রিক পরিবর্তন, কিন্তু একত্রে এগুলো ভঙ্গুর asynchronous স্ক্রিপ্টগুলোকে এমন কোডে রূপান্তরিত করে যা আপনি প্রকৃতপক্ষে বিশ্বাস করতে পারেন।
