সরাসরি পর্বে যান
পর্ব ৩২ · ৫৯-এর মধ্যে · মডিউল ৫

callback, setTimeout আর callback hell — “কাজ শেষ হলে আমাকে ডাকুন”

JavaScript Callbacks & Timers — setTimeout, setInterval, Error-First Callbacks, Zalgo, Inversion of Control & the Pyramid of Doom

This Bangla lesson covers asynchronous programming before promises: sync versus async callbacks, setTimeout and setInterval with timer ids, drift and recursive timers, an OTP countdown, Node.js error-first callbacks and EventEmitter, the Zalgo and inversion-of-control problems, recognising and flattening callback hell with named functions, a parallel counter, and bridging to promises with util.promisify.

এই পর্বে যা শিখবেন

  • Sync callback (array method) আর async callback (timer, event, I/O)-এর পার্থক্য, আর event loop-এর সাথে তাদের সম্পর্ক
  • setTimeout, clearTimeout, setInterval, clearInterval — delay-এর আসল অর্থ, timer id, drift আর OTP countdown
  • Node.js-এর error-first callback আর EventEmitter — ফাইল পড়া আর event দিয়ে
  • Zalgo আর inversion of control — callback-এর বিশ্বাসের সংকট, আর once দিয়ে প্রতিরক্ষা
  • Callback hell চেনা, named function দিয়ে সামলানো, parallel counter লেখা, আর Promise-এর দিকে সেতু util.promisify

১ · Callback কী — “কাজ শেষ হলে আমাকে ডাকুন”

পর্ব ১৩ আর ১৬-এ আপনি callback দেখেছেন: একটি function, যা আরেকটি function-কে argument হিসেবে দেওয়া হয়, যাতে সে নিজের সুবিধামতো সময়ে সেটিকে ডাকে। [3, 1, 2].sort((a, b) => a - b)-এ তুলনার function একটি callback; prices.map((p) => p * 1.15)-এ ভ্যাট যোগের function-ও তাই। যে function অন্য function নেয়, সেটি একটি higher-order function.

কিন্তু এই পর্বের callback একটু আলাদা জাতের। পর্ব ৩১-এ দেখেছেন, JavaScript-এর মূল কোড চলে একটিমাত্র thread-এ (single-threaded): একসাথে একটিই কাজ। তাহলে একটি ফাইল পড়তে ২ সেকেন্ড লাগলে কি পুরো পাতা ২ সেকেন্ড জমে থাকবে? না। JavaScript কাজটি ব্রাউজার বা Node.js-এর হাতে তুলে দেয় (Web API বা Node-এর libuv), সাথে দেয় একটি function — “শেষ হলে এটি ডাকবেন।” তারপর নিজে পরের লাইনে চলে যায়। কাজ শেষ হলে callback-টি task queue-তে লাইনে দাঁড়ায়, আর call stack খালি হলে event loop তাকে চালায়। এই “এখন শুরু, পরে শেষ” ধরনের কোডকে বলে asynchronous (async) কোড।

পুরান ঢাকার একটি বিরিয়ানির দোকানে ভিড়। আপনি অর্ডার দিলেন, কাউন্টারের ভাই আপনার ফোন নম্বর লিখে নিলেন: “হলে ফোন দিব।” আপনি দাঁড়িয়ে না থেকে পাশের দোকানে চা খেতে গেলেন। বিরিয়ানি প্যাক হলে ফোন এল, আপনি এসে নিলেন। এখানে ফোন নম্বরটিই callback — কাজ শেষ হলে কাকে কীভাবে জানাতে হবে। আর কাউন্টারের ভাই একা হাতে (একটি thread) শত গ্রাহক সামলাচ্ছেন, কারণ কাউকেই তাঁর সামনে দাঁড় করিয়ে রাখছেন না।

Sync callback বনাম async callback

একই শব্দ “callback”, কিন্তু দুটি সম্পূর্ণ ভিন্ন সময়-আচরণ। পার্থক্যটি মাথায় পরিষ্কার না থাকলে async কোডের প্রায় সব বাগ বোঝা কঠিন হয়:

বিষয়Sync callbackAsync callback
উদাহরণmap, filter, forEach, sort, reducesetTimeout, addEventListener, fs.readFile, পুরোনো XMLHttpRequest
কখন চলেএখনই — higher-order function ফেরত আসার আগেই সব কল শেষপরে — বর্তমান কোড আর call stack শেষ হওয়ার পরে
ক্রমপরের লাইনের আগেপরের লাইনের পরে, সবসময়
throw করলেচারপাশের try/catch ধরতে পারেচারপাশের try/catch ততক্ষণে শেষ — ধরতে পারে না
ফল ফেরতreturn দিয়ে, সরাসরিreturn কাজে লাগে না — ফল যায় callback-এর argument হিসেবে
console.log("১. শুরু");
[10, 20].forEach((x) => console.log("২. sync callback", x));
setTimeout(() => console.log("৪. async callback"), 0);
console.log("৩. শেষ লাইন");
// প্রত্যাশিত আউটপুট: ১, ২ (10), ২ (20), ৩, তারপর ৪

forEach-এর callback দুটি চলে forEach ফেরত আসার আগেই। কিন্তু setTimeout(…, 0)-এর callback ০ ms delay সত্ত্বেও চলে সবার শেষে — কারণ সেটি task queue-তে যায়, আর event loop তাকে নেয় শুধু তখন, যখন বর্তমান script পুরো শেষ। পর্ব ৩১-এর মূল নিয়মটিই এখানে কাজ করছে: চলমান কোড কখনো মাঝপথে থামিয়ে callback চালানো হয় না (run-to-completion).

Async callback কোথা থেকে আসে — তিনটি উৎস

⏱️ Timer

setTimeout, setInterval — “এত সময় পরে ডাকবেন।” সবচেয়ে সরল async API, তাই শেখার শুরু এখান থেকে।

🖱️ Event

ক্লিক, কি-চাপ, পাতা লোড, Node-এর 'data' — “যখনই ঘটবে, ডাকবেন।” একবার নয়, বারবার আসতে পারে।

💾 I/O

ফাইল পড়া, ডেটাবেস query, নেটওয়ার্ক — “উত্তর এলে ডাকবেন, সাথে ফল বা ভুল দেবেন।” Node.js-এ এর নিয়ম error-first (অংশ ৫)।

মূল ধারণা

Async callback মানে ভবিষ্যতের জন্য রেখে যাওয়া নির্দেশ। আপনি “এখন” কোড লেখেন, কিন্তু callback-এর ভেতরের অংশ চলে “পরে”, যখন চারপাশের কোড অনেক আগে শেষ। এজন্য ফল return দিয়ে বাইরে আনা যায় না — যা করার, callback-এর ভেতরেই করতে হয়। এই একটি সীমাবদ্ধতা থেকেই পরে callback hell জন্মায়।

C/C++/Python/Java জানলে

C-তে callback মানে function pointer: qsort-এর comparator বা signal handler; প্রসঙ্গ পাঠাতে আলাদা void * লাগে, কারণ closure নেই (C-এর function pointer আর callback). Java-তে একই ধারণা listener interface (Swing-এর ActionListener) আর lambda; async কাজের জন্য আধুনিক Java ব্যবহার করে CompletableFuture, যা JavaScript-এর Promise-এর আত্মীয় (Java-র Executor আর CompletableFuture). Python-এর asyncio-তেও loop.call_later আর add_done_callback-এর মতো callback আছে, কিন্তু দৈনন্দিন কোড লেখা হয় async/await দিয়ে (Python-এর asyncio). বড় পার্থক্য: JavaScript-এ async callback চলে একই thread-এ, event loop-এর পালা অনুযায়ী — Java-র মতো আলাদা thread-এ নয়।

২ · setTimeout আর clearTimeout — delay মানে “অন্তত এতক্ষণ”

setTimeout একটি function আর একটি সময় (millisecond-এ) নেয়: “অন্তত এত ms পরে এই function-টি চালাবেন।” এটি নিজে কোনো অপেক্ষা করে না — timer-টি রেজিস্টার করে সাথে সাথে ফেরত আসে, আর আপনার কোড পরের লাইনে চলে যায়।

// ১) সাধারণ ব্যবহার
const id = setTimeout(() => console.log("চা তৈরি!"), 3000);

// ২) callback-এ argument পাঠানো — delay-এর পরের সব argument callback পায়
setTimeout((name, n) => console.log(`${name}, আপনার টোকেন ${n}`), 500, "রহিম", 42);

// ৩) মত বদলালে থামানো — callback আর কখনো চলবে না
clearTimeout(id);

// ৪) কখনো এভাবে নয়: string দিলে eval-এর মতো চলে (নিরাপত্তা ঝুঁকি, ধীর)
// setTimeout("console.log('hi')", 100);

দ্বিতীয় লাইনে খেয়াল করুন: setTimeout(fn, delay, arg1, arg2) — delay-এর পরের argument-গুলো callback-কে দেওয়া হয়। অনেকে ভুল করে লেখেন setTimeout(greet("রহিম"), 500); এতে greet এখনই চলে, আর তার ফেরত মান (সাধারণত undefined) timer-এর হাতে যায়। Function পাঠাতে হয়, function-এর ফল নয় — তাই হয় arrow দিয়ে মুড়ুন (() => greet("রহিম")), নয়তো argument আলাদা করে দিন।

Delay একটি ন্যূনতম সময়, প্রতিশ্রুতি নয়

setTimeout(fn, 1000) মানে “১০০০ ms-এর আগে নয়।” ঠিক ১০০০ ms-এ চলবে, এমন কোনো নিশ্চয়তা নেই। কারণগুলো:

  • Main thread ব্যস্ত থাকলে অপেক্ষা। সময় পূর্ণ হলে callback শুধু task queue-তে ঢোকে। তখন যদি একটি ভারী লুপ ৩ সেকেন্ড ধরে চলে (long task), callback সেই ৩ সেকেন্ড লাইনে দাঁড়িয়ে থাকবে।
  • ০ মানে “যত তাড়াতাড়ি সম্ভব, কিন্তু এখন নয়।” setTimeout(fn, 0) বর্তমান কোড আর সব microtask শেষ হওয়ার পরে চলে।
  • Nested timer-এ ৪ ms (timer clamping). HTML মানদণ্ড অনুযায়ী timer-এর ভেতর থেকে timer, এভাবে ৫ স্তরের বেশি গভীরে গেলে ব্রাউজার delay অন্তত ৪ ms করে দেয়।
  • পেছনের ট্যাবে আরও ধীর। ট্যাবটি চোখের আড়ালে থাকলে ব্যাটারি বাঁচাতে ব্রাউজার timer ধীর করে দেয় — সাধারণত সেকেন্ডে একবারের বেশি নয়, দীর্ঘক্ষণ লুকানো থাকলে আরও কম।
  • সর্বোচ্চ সীমা। Delay রাখা হয় 32-bit signed integer-এ, তাই সর্বোচ্চ 2147483647 ms (প্রায় ২৪.৮ দিন)। এর বেশি দিলে overflow হয়ে timer প্রায় সাথে সাথে চলে যায় (Node.js একটি TimeoutOverflowWarning দিয়ে delay ১ করে দেয়)।

Timer id — ব্রাউজারে সংখ্যা, Node.js-এ object

setTimeout একটি timer id ফেরত দেয় — এটি দিয়েই পরে clearTimeout(id) করে timer বাতিল করা হয়। ব্রাউজারে id একটি ধনাত্মক পূর্ণসংখ্যা (যেমন 7). Node.js-এ এটি একটি Timeout object, যার কিছু বাড়তি method আছে: timeout.unref() বললে এই timer একা process-কে বাঁচিয়ে রাখবে না, refresh() বললে গোনা আবার শুরু থেকে হবে। তাই timer id-কে কখনো “সংখ্যা” ধরে নিয়ে হিসাব করবেন না — শুধু রেখে দিন আর clear-এ ফেরত দিন।

যে timer-এর id হারিয়েছে, তাকে আর থামানো যায় না। React বা Vue component-এ timer চালু করে id না রাখলে, component সরে যাওয়ার পরেও timer চলতে থাকে, আর মুছে যাওয়া component-এর state বদলাতে চায়। নিয়ম: যে timer শুরু করছেন, তার id রাখুন আর থামানোর পথ ঠিক রাখুন।

৩ · setInterval, drift আর recursive setTimeout

setInterval একই callback নির্দিষ্ট সময় পরপর বারবার চালায়, যতক্ষণ না clearInterval(id) দিয়ে থামানো হয়। ঘড়ি, countdown, প্রতি ৩০ সেকেন্ডে নতুন বার্তা আছে কি না দেখা — শুনতে সহজ। কিন্তু দুটি জিনিস প্রায়ই ভুল বোঝা হয়।

প্রথমত, setInterval আপনার কাজ শেষ হওয়ার অপেক্ষা করে না। সে একটি নির্দিষ্ট ছন্দ ধরে রাখার চেষ্টা করে — ০, ১০০, ২০০, ৩০০ ms… কিন্তু callback-এর কাজে যদি interval-এর চেয়ে বেশি সময় লাগে? তখন পরের tick-এর সময় পেরিয়ে যায়, আর কাজ শেষ হতেই প্রায় সাথে সাথে আবার শুরু হয় — মাঝে কোনো বিরতি নেই। Main thread একটানা ব্যস্ত থাকে, ক্লিক আর পাতা আঁকার সুযোগ পায় না। সমাধান হলো recursive setTimeout: কাজ শেষ হলে তবেই পরের timer বসানো। এতে প্রতিটি কাজের পরে অন্তত নির্দিষ্ট বিরতি নিশ্চিত থাকে। ধাপে ধাপে দেখুন। “পরে” চাপুন।

1const work = () => busy(150);  // ১৫০ ms-এর কাজ2setInterval(work, 100);34function loop() {5  work();6  setTimeout(loop, 100);   // কাজের পরে7}8setTimeout(loop, 100);
সময়রেখা (দুটি আলাদা প্রোগ্রাম ধরে নিন)
সময়
setInterval
recursive setTimeout
Main thread-এ বিরতি
  1. শুরু — ০ ms. setInterval(work, 100) একটি ছন্দ বসায়: প্রতি ১০০ ms-এ একবার। Recursive সংস্করণে বসানো হয় মাত্র একটি timer — পরেরটি বসবে কাজ শেষ হলে।
  2. ১০০ ms — প্রথম কাজ। দুই প্রোগ্রামেই work() শুরু, চলবে ২৫০ ms পর্যন্ত। এই সময় thread পুরো দখলে; JavaScript-এ কোনো callback অন্য কোডকে মাঝপথে থামিয়ে ঢুকতে পারে না।
  3. ২০০ ms — interval-এর পরের tick-এর সময়। সময় হয়েছে, কিন্তু thread ব্যস্ত, তাই tick queue-তে অপেক্ষা করে। ব্রাউজার সাধারণত একই interval-এর অনেকগুলো জমা tick স্তূপ করে রাখে না — বাদ পড়া tick হারিয়ে যায়। Recursive সংস্করণে এই মুহূর্তে কোনো timer-ই নেই।
  4. ২৫০ ms — পার্থক্য এখানে। প্রথম কাজ শেষ। Interval-এর অপেক্ষমাণ tick সাথে সাথে চলে — কাজের মাঝে শূন্য বিরতি। Recursive সংস্করণে লাইন ৬ এখন পরের timer বসায়, ৩৫০ ms-এর জন্য। মাঝের ১০০ ms-এ thread খালি: ক্লিক, স্ক্রল আর রেন্ডার হতে পারে।
  5. দীর্ঘ সময়ে। Interval-এর কাজ একটার পর একটা লেগে থাকে, পাতা জমে যায়। Recursive setTimeout প্রতিটি কাজের পরে বিরতি নিশ্চিত করে, আর ছন্দ নিজে থেকেই কাজের গতির সাথে মানিয়ে নেয়। তাই নেটওয়ার্ক polling বা ভারী কাজে recursive setTimeout ভালো; হালকা কাজে (ঘড়ির কাঁটা) setInterval ঠিক আছে।
setInterval: “প্রতি ১০০ ms-এ শুরু” · recursive setTimeout: “শেষ করে ১০০ ms বিরতি, তারপর আবার”

দ্বিতীয়ত, tick গুনে সময় মাপা যায় না। প্রতিটি tick সামান্য দেরিতে আসে, আর দেরি জমতে থাকে — একে বলে timer drift. পেছনের ট্যাবে timer ধীর হলে দেরি অনেক বড় হয়। তাই OTP countdown-এর মতো জিনিসে “৬০ বার tick গুনলে ৬০ সেকেন্ড” ভাবা ভুল। নির্ভরযোগ্য উপায়: শুরুতে শেষ সময়টি লিখে রাখুন (const end = Date.now() + 60_000), আর প্রতি tick-এ Math.ceil((end - Date.now()) / 1000) হিসাব করে বাকি সেকেন্ড দেখান। Tick দেরিতে এলেও দেখানো সময় ঠিক থাকে।

চালিয়ে দেখুন — OTP countdown আর দ্রুত ডেলিভারি

নিচের কোডে দুটি বাস্তব উদাহরণ। প্রথমটি OTP-র মেয়াদ গোনে (পাতায় সময় বাঁচাতে ৬০ সেকেন্ডের বদলে ৩ tick, প্রতিটি ১০০ ms) আর শেষে নিজেকে থামায়। দ্বিতীয়টি একটি ডেলিভারির তিনটি ধাপ পরপর চালায় — প্রতিটি ধাপ আগেরটি শেষ হলে তবেই শুরু, তাই callback-এর ভেতরে callback. প্রথম ধাপের delay ইচ্ছা করে ৪৫০ ms রাখা হয়েছে, যাতে countdown শেষ হওয়ার পরে ডেলিভারি শুরু হয় আর আউটপুট পড়তে সহজ হয়।

// ১) OTP countdown — setInterval চলে, শেষে clearInterval
let left = 3;
const otpTimer = setInterval(() => {
  if (left > 0) return console.log(`OTP-এর মেয়াদ বাকি: ${left--}`);
  clearInterval(otpTimer);                    // না থামালে চিরকাল চলবে
  console.log("মেয়াদ শেষ — আবার কোড চান");
}, 100);

// ২) দ্রুত ডেলিভারি — ধাপগুলো পরপর, তাই callback-এর ভেতরে callback
function step(label, ms, next) {
  setTimeout(() => { console.log(label); next(); }, ms);
}
step("📦 অর্ডার প্যাক হলো", 450, () => {
  step("🚚 গাড়িতে উঠল", 100, () => {
    step("🏠 দরজায় পৌঁছাল", 100, () => {
      console.log("ডেলিভারি সম্পূর্ণ");
    });
  });
});
console.log("sync কোড শেষ — timer-গুলো এখনো অপেক্ষায়");

প্রথম লাইনটি আসে শেষ console.log থেকে: sync কোড শেষ — timer-গুলো এখনো অপেক্ষায় — সব timer শুধু বসানো হয়েছে, কোনোটি এখনো চলেনি। তারপর ১০০, ২০০, ৩০০ ms-এ OTP-এর মেয়াদ বাকি: 3, 2, 1; ৪০০ ms-এ left শূন্য, তাই clearInterval চলে আর আসে মেয়াদ শেষ — আবার কোড চান. clearInterval না থাকলে এই callback প্রতি ১০০ ms-এ চিরকাল চলত — পাতা বন্ধ না হওয়া পর্যন্ত। এরপর ৪৫০, ৫৫০, ৬৫০ ms-এ 📦 অর্ডার প্যাক হলো, 🚚 গাড়িতে উঠল, 🏠 দরজায় পৌঁছাল, আর শেষে ডেলিভারি সম্পূর্ণ. ক্রম নিশ্চিত, কারণ প্রতিটি step ডাকা হয় আগের ধাপের callback-এর ভেতর থেকে. কিন্তু দেখুন, মাত্র তিনটি ধাপেই কোড ডানদিকে তিন স্তর সরে গেছে — অংশ ৭-এর callback hell-এর বীজ এখানেই।

N বার পরে থামা, আর recursive সংস্করণ

“ঠিক ৫ বার মনে করিয়ে দিন” — এমন কাজে একটি counter রেখে শর্ত পূর্ণ হলে clearInterval করুন, যেমন উপরের OTP-তে। Recursive setTimeout-এ থামানো আরও সহজ: শর্ত পূর্ণ হলে পরের timer আর বসাবেন না।

function poll(tries = 0) {
  checkPayment((err, paid) => {               // ধরা যাক bKash পেমেন্টের অবস্থা দেখে
    if (paid) return console.log("পেমেন্ট পাওয়া গেছে");
    if (tries >= 4) return console.log("সময় শেষ — পরে আবার দেখুন");
    setTimeout(() => poll(tries + 1), 2000);  // উত্তর আসার পরেই পরের চেষ্টা
  });
}

এখানে একটি সূক্ষ্ম সুবিধা আছে: নেটওয়ার্কের উত্তর আসতে ৫ সেকেন্ড লাগলেও কখনো দুটি অনুরোধ একসাথে চলবে না, কারণ পরের অনুরোধ বসে আগেরটির উত্তর আসার পরে। setInterval(check, 2000) দিলে ধীর নেটওয়ার্কে একসাথে কয়েকটি অনুরোধ ঝুলে থাকত, আর উত্তরগুলো এলোমেলো ক্রমে ফিরতে পারত।

৪ · Timer আর this, আর event — callback-এর আরেক রূপ

Timer-এ হারানো this

পর্ব ২৪-এ দেখেছেন, সাধারণ function-এর this ঠিক হয় ডাকার ধরন দেখে। setTimeout(cart.report, 1000) লিখলে timer-এর হাতে যায় শুধু function-টি, cart নয়। এক সেকেন্ড পরে timer সেটিকে ডাকে cart. ছাড়াই — ব্রাউজারে this হয় global object, Node.js-এ একটি Timeout object. একে বলে হারানো this.

class Countdown {
  constructor(sec) { this.sec = sec; }
  startBroken() { setInterval(this.tick, 1000); }        // ❌ this হারায়
  start()       { this.id = setInterval(() => this.tick(), 1000); }  // ✅ arrow
  tick() {
    console.log(this.sec--);
    if (this.sec < 0) clearInterval(this.id);
  }
}
// startBroken: tick-এর ভেতরে this.sec → undefined, তারপর NaN …

start-এ arrow function নিজের this বানায় না, চারপাশের — অর্থাৎ start-এর — this ব্যবহার করে, যা object-টি নিজে (পর্ব ২৪-এর lexical this). ভেতরে this.tick() ডাকটি আবার obj.method() আকারে হয়, তাই tick-এর ভেতরেও this ঠিক থাকে। আরেকটি পথ setInterval(this.tick.bind(this), 1000). লক্ষ করুন, id-টি this.id-এ রাখা হয়েছে, যাতে tick নিজেই নিজেকে থামাতে পারে।

Event — বারবার ডাকা callback

Timer একবার বা নির্দিষ্ট ছন্দে ডাকে। Event-এর callback ডাকা হয় “যখনই ঘটনাটি ঘটবে” — হয়তো কখনোই না, হয়তো সেকেন্ডে দশবার। ব্রাউজারে এর রূপ event listener:

const btn = document.querySelector("#resend");
function onResend() { console.log("নতুন OTP পাঠানো হলো"); }
btn.addEventListener("click", onResend);       // প্রতিটি ক্লিকে একবার
btn.removeEventListener("click", onResend);    // থামাতে একই function reference লাগে

Event-এর bubbling, delegation আর custom event পর্ব ৩৮-এ বিস্তারিত। Node.js-এ একই নকশার নাম EventEmitter: একটি object, যাতে on(নাম, fn) দিয়ে listener লাগানো যায় আর emit(নাম, ডেটা) দিয়ে ঘটনা ঘোষণা করা যায়। Node-এর stream, HTTP server, process — প্রায় সবকিছু EventEmitter. একটি গুরুত্বপূর্ণ তথ্য, যা অনেকে জানেন না: emit নিজে synchronous — সে listener-গুলোকে রেজিস্টারের ক্রমে, সাথে সাথে, একই call stack-এ ডাকে। Event “পরে” আসে শুধু তখন, যখন emit-কে কোনো async উৎস (ফাইল, নেটওয়ার্ক, timer) পরে ডাকে। পরের অংশের Node কোডে এটি নিজের চোখে দেখবেন।

৫ · Node.js-এর error-first callback — ভুল আগে, ফল পরে

Timer কখনো ব্যর্থ হয় না — সময় হলে চলেই। কিন্তু ফাইল পড়া ব্যর্থ হতে পারে: ফাইল নেই, অনুমতি নেই, ডিস্ক ভরা। Async কাজে ব্যর্থতা কীভাবে জানানো হবে? throw কাজ করে না — ভুলটি ঘটে অনেক পরে, যখন কলকারী function আর তার try/catch অনেক আগেই শেষ। তাই Node.js একটি রীতি দাঁড় করায়, যার নাম error-first callback:

  • Callback-এর প্রথম parameter সবসময় ভুল (err). সফল হলে সেখানে null (বা undefined).
  • সফল হলে ফল আসে দ্বিতীয় (প্রয়োজনে তৃতীয়…) parameter-এ।
  • Callback একবারই ডাকা হয় — হয় ভুল দিয়ে, নয় ফল দিয়ে, দুটো একসাথে নয়।
  • যে callback নেয়, সেই function-এর শেষ parameter হলো callback.

ভুল প্রথমে কেন? কারণ তাহলে ভুলটি উপেক্ষা করা কঠিন — callback লিখতে বসলেই প্রথম নামটি err, আর চোখের সামনে থাকে। ফলের সংখ্যা কাজভেদে বদলাতে পারে (কোনো কাজ শুধু “হয়েছে” জানায়, কোনোটি দুটি মান দেয়), কিন্তু ভুলের জায়গা সব API-তে একই থাকে — তাই util.promisify-এর মতো সাধারণ যন্ত্র যেকোনো callback API-কে একইভাবে বুঝতে পারে (অংশ ৮)। এর ইতিহাস ভাবনার প্রশ্ন ২-এ।

নিচের কোডটি Node.js 20-এ চলে। এটি প্রথমে একটি config ফাইল লেখে, তারপর error-first callback দিয়ে পড়ে; তারপর ইচ্ছা করে একটি অনুপস্থিত ফাইল পড়ে ভুলের পথ দেখায়। সাথে একটি EventEmitter — দেখুন কোন আউটপুট কখন আসে।

const fs = require("node:fs");
const EventEmitter = require("node:events");
fs.writeFileSync("config.json", '{ "shop": "দ্রুত ডেলিভারি", "vat": 15 }');

function loadConfig(file, cb) {                 // error-first callback
  fs.readFile(file, "utf8", (err, text) => {
    if (err) return cb(err);                    // ভুল হলে আগেই ফেরত
    let cfg;
    try { cfg = JSON.parse(text); } catch (e) { return cb(e); }
    cb(null, cfg);                              // সফল: প্রথম ঘরে null
  });
}
loadConfig("config.json", (err, cfg) => {
  if (err) return console.log("ভুল:", err.message);
  console.log("config:", cfg.shop, cfg.vat);
  loadConfig("missing.json", (err2) => console.log("ভুল:", err2.code));
});

const order = new EventEmitter();
order.on("placed", (id) => console.log("SMS গেল, অর্ডার", id));
order.once("placed", (id) => console.log("প্রথম অর্ডারে উপহার:", id));
order.emit("placed", 101);
order.emit("placed", 102);
console.log("emit শেষ — readFile-এর callback এখনো আসেনি");

আউটপুটের প্রথম চার লাইন আসে EventEmitter থেকে: SMS গেল, অর্ডার 101, প্রথম অর্ডারে উপহার: 101, SMS গেল, অর্ডার 102, তারপর emit শেষ — readFile-এর callback এখনো আসেনি. অথচ কোডে loadConfig আগে লেখা! কারণ fs.readFile async — কাজটি libuv-এর হাতে গেছে — আর emit sync, তাই listener-গুলো সাথে সাথে চলেছে। once দিয়ে লাগানো listener প্রথম emit-এর পরে নিজেকে সরিয়ে নিয়েছে, তাই ১০২-এ উপহার নেই। এরপর ফাইল পড়া শেষ হলে আসে config: দ্রুত ডেলিভারি 15, আর তার callback-এর ভেতর থেকে দ্বিতীয় পড়া শুরু হয় বলে ক্রম নিশ্চিত: শেষে ভুল: ENOENT — Node-এর “Error NO ENTry”, অর্থাৎ ফাইল নেই। দুটি সূক্ষ্ম বিষয় দেখুন: (১) JSON.parse-এর ভুলও try/catch দিয়ে ধরে callback-এ পাঠানো হয়েছে — async function-এর সব ভুল একই পথে যাওয়া উচিত। (২) cb(null, cfg) ইচ্ছা করে try-এর বাইরে; ভেতরে থাকলে callback নিজে কোনো ভুল ছুড়লে catch সেটিকে ধরে আবার cb(e) ডাকত — একই callback দুবার!

সবচেয়ে সাধারণ ভুল: err উপেক্ষা করা। fs.readFile(f, "utf8", (err, text) => show(text.length)) — ফাইল না থাকলে text হয় undefined, আর আসল কারণ (ENOENT) হারিয়ে গিয়ে আসে একটি বিভ্রান্তিকর TypeError. আরও খারাপ: async callback-এর ভেতরে ছোড়া ভুল কোনো বাইরের try/catch ধরে না — এটি uncaught exception হয়ে পুরো Node.js process বন্ধ করে দেয়। প্রতিটি error-first callback-এর প্রথম লাইন: if (err) return …. ফাইল না থাকা, নেটওয়ার্ক বিচ্ছিন্ন হওয়ার মতো প্রত্যাশিত ভুলগুলো operational error — এগুলো সামলানোর পরিকল্পনা আগে থেকেই থাকা উচিত (পর্ব ২৯)।

৬ · Zalgo আর inversion of control — callback-এর বিশ্বাসের সংকট

Zalgo — কখনো sync, কখনো async

একটি দামের API ভাবুন, যা cache-এ দাম থাকলে সাথে সাথে callback ডাকে, না থাকলে সার্ভার থেকে এনে পরে ডাকে। শুনতে কার্যকর — cache hit-এ অপেক্ষা কেন? কিন্তু এতে callback কখনো sync, কখনো async হয়ে যায়, আর কলকারীর কোডের ক্রম নির্ভর করে cache-এ কী আছে তার ওপর। Node.js-এর তৎকালীন প্রধান Isaac Schlueter এই সমস্যার নাম দেন Zalgo — একটি ইন্টারনেট মিম থেকে নেওয়া, যেখানে “Zalgo” মানে বিশৃঙ্খলার দানব। ধাপে ধাপে দেখুন:

1function getPrice(item, cb) {2  if (cache.has(item)) return cb(cache.get(item));3  // ✅ queueMicrotask(() => cb(cache.get(item)))4  fetchPrice(item, cb);           // async5}6console.log("A");7getPrice(item, (p) => { spinner.stop(); console.log("B", p); });8spinner.start();9console.log("C");
কী ঘটছে
item
callback কখন
spinner
  1. Cache hit — sync পথ। “tea” cache-এ আছে, তাই লাইন ২ সাথে সাথে callback ডাকে — getPrice ফেরত আসার আগেই। আউটপুট A, B 20, C. Callback-এর ভেতরে spinner.stop() চলে গেছে লাইন ৮-এর spinner.start()-এর আগে!
  2. Cache miss — async পথ। “rice” নেই, তাই সার্ভারে অনুরোধ যায়। getPrice ফেরত আসে, লাইন ৮-৯ চলে, তারপর উত্তর এলে callback. আউটপুট A, C, B 50 — spinner ঠিকমতো শুরু হয়ে থামে। ডেভেলপার এই পথেই টেস্ট করেছিলেন, তাই সব ঠিক মনে হয়েছিল।
  3. বাগটি দেখা দেয় শুধু মাঝে মাঝে। দ্বিতীয়বার একই জিনিস দেখলে cache hit হয়, আর spinner চিরকাল ঘুরতে থাকে। বাগটি নির্ভর করে ব্যবহারকারী আগে কী দেখেছেন তার ওপর — আবার তৈরি করা কঠিন, ধরা আরও কঠিন। এটাই Zalgo: কলকারী জানে না callback-এর আগে না পরে তার পরের লাইন চলবে।
  4. সমাধান — সবসময় async. Cache hit হলেও callback-কে পরে পাঠান: queueMicrotask(() => cb(…)) (Node.js-এ process.nextTick-ও চলে)। এখন দুই পথেই ক্রম A, C, B. Microtask খুব তাড়াতাড়ি চলে, তাই গতির ক্ষতি সামান্য, কিন্তু আচরণ নিশ্চিত। নিয়ম: যে callback কখনো async, সেটি সবসময় async হোক।

queueMicrotask একটি function-কে microtask queue-তে পাঠায় — বর্তমান কোড শেষ হলেই, কোনো timer বা I/O-র আগে চলে (পর্ব ৩১)। Node.js-এর পুরোনো সমতুল্য process.nextTick, যা আরও আগে, Promise-এর microtask-গুলোরও আগে চলে। পর্ব ৩৩-এ দেখবেন, Promise-এর then callback মানদণ্ড অনুযায়ী সবসময় async — Zalgo-কে ভাষার স্তরেই নিষিদ্ধ করা হয়েছে।

Inversion of control — নিজের কোডের নিয়ন্ত্রণ অন্যের হাতে

আপনি যখন একটি third-party লাইব্রেরিকে callback দেন, তখন একটি গুরুত্বপূর্ণ জিনিস তুলে দেন: আপনার কোডের বাকি অংশ কখন, কতবার, কোন argument দিয়ে চলবে — সেই সিদ্ধান্ত। একে বলে inversion of control. সাধারণত আপনি function ডাকেন; এখানে অন্যের কোড আপনার function ডাকে। লাইব্রেরি ভালো হলে সমস্যা নেই। কিন্তু ধরুন একটি পেমেন্ট SDK:

  • দুবার ডাকে — একটি retry বাগে onSuccess দুবার; আপনার কোড দুটি অর্ডার তৈরি করে, গ্রাহকের কাছে দুটি SMS যায়।
  • কখনো ডাকে না — নেটওয়ার্ক বিচ্ছিন্ন হলে SDK চুপচাপ থেমে যায়; আপনার “অপেক্ষা করুন” লেখা চিরকাল থাকে।
  • খুব তাড়াতাড়ি ডাকে — Zalgo-র মতো, আপনার প্রস্তুতি শেষ হওয়ার আগেই।
  • ভুল argument দেয় — err আর data দুটোই দেয়, বা কোনোটিই না; বা আপনার callback-এর ভুল নিজে চুপচাপ গিলে ফেলে।

Callback দিয়ে এর প্রতিরক্ষা নিজেকেই লিখতে হয়। পর্ব ২২-এর once মনে আছে? সেটিকে একটি timeout-সহ রক্ষাকবচ বানানো যায়:

function guarded(cb, ms = 10_000) {
  let called = false;                                 // closure-এ রাখা পাহারাদার
  const timer = setTimeout(() => finish(new Error("সময় শেষ — উত্তর আসেনি")), ms);
  function finish(err, data) {
    if (called) return;                               // দ্বিতীয় কল উপেক্ষা
    called = true;
    clearTimeout(timer);
    queueMicrotask(() => cb(err, data));              // সবসময় async — Zalgo নয়
  }
  return finish;
}
paymentSdk.charge(500, guarded((err, receipt) => { /* একবারই, নিশ্চিতভাবে */ }));

তিনটি সমস্যা একসাথে সামলানো হলো: called flag দিয়ে “দুবার” (closure, পর্ব ২২), timer দিয়ে “কখনো না”, আর queueMicrotask দিয়ে “খুব তাড়াতাড়ি”। এটি defensive programming-এর একটি উদাহরণ। কিন্তু খেয়াল করুন, প্রতিটি callback API-এর জন্য এই পাহারা নিজে বসাতে হচ্ছে। Promise এই তিনটি নিশ্চয়তা ভাষার ভেতরেই দেয়: একটি promise একবারই settle হয়, then-callback সবসময় async, আর ভেতরের throw ধরা পড়ে rejection হয়ে যায়।

৭ · Callback hell — পিরামিডের অভিশাপ

এবার একটি পুরো অনলাইন অর্ডার ভাবুন। পাঁচটি ধাপ, প্রতিটি async, আর প্রতিটি আগের ধাপের ফলের ওপর নির্ভরশীল: ব্যবহারকারী আনা → তার cart আনা → স্টক আছে কি না দেখা → অর্ডার জমা দেওয়া → SMS পাঠানো। Callback দিয়ে “আগেরটি শেষ হলে পরেরটি” বলার একমাত্র উপায় হলো পরেরটিকে আগেরটির callback-এর ভেতরে লেখা। ফল:

getUser(userId, (err, user) => {
  if (err) return fail(err);
  getCart(user.id, (err, cart) => {
    if (err) return fail(err);
    checkStock(cart.items, (err, ok) => {
      if (err) return fail(err);
      placeOrder(user, cart, (err, order) => {
        if (err) return fail(err);
        sendSms(user.phone, `অর্ডার ${order.id} নিশ্চিত`, (err) => {
          if (err) console.log("SMS যায়নি, তবে অর্ডার হয়েছে");
          showDone(order);
        });
      });
    });
  });
});

কোড ডানদিকে সরতে সরতে একটি পাশ-ফেরা পিরামিডের আকার নেয় — এর নাম callback hell, বা “pyramid of doom”. শুধু দেখতে খারাপ নয়; এর আসল সমস্যাগুলো গভীরে। একটি ভুলের যাত্রা ধাপে ধাপে দেখুন — ধরা যাক তৃতীয় ধাপে স্টক নেই।

1getUser(id, (err, user) => {2  if (err) return fail(err);3  getCart(user.id, (err, cart) => {4    if (err) return fail(err);5    checkStock(cart.items, (err, ok) => {6      if (err) return fail(err);   // ← এখানে7      placeOrder(user, cart, (err, order) => {8        if (err) return fail(err);9        sendSms(user.phone, …, (err) => {…});10      });11    });12  });13});  // চারপাশে try/catch দিলে?
ভুলের যাত্রা
ধাপ
err
গভীরতা
ফল
  1. ধাপ ১ — সফল। getUser-এর callback-এ err = null, তাই লাইন ২ পেরিয়ে স্তর ২-এ যাওয়া। প্রতিটি স্তরের প্রথম কাজ if (err) — এই পাহারা প্রতিটি স্তরে আলাদা করে বসাতে হয়।
  2. ধাপ ২ — সফল। Cart পাওয়া গেল। লক্ষ করুন, ভেতরের callback-এর err বাইরের err-কে আড়াল করছে (shadowing, পর্ব ৭) — পাঁচটি err, পাঁচটি আলাদা variable.
  3. ধাপ ৩ — ভুল, আর ঠিক জায়গায় ধরা পড়ল। checkStock callback ডাকে new Error("স্টক নেই") দিয়ে। লাইন ৬-এর if (err) return fail(err) ভুলটি ধরে আর return দিয়ে বেরিয়ে যায় — placeOrder আর sendSms আর চলে না। ভুলটি ধরা গেল শুধু এই স্তরেই; অন্য কোনো স্তর এর খবর জানে না।
  4. যদি লাইন ৬ ভুলে যেতেন। AI-এর কোডে বা তাড়াহুড়োয় এমন হয়ই। err কেউ দেখল না, ok হলো undefined, আর কোড চলে গেল placeOrder-এ — স্টক না থাকা জিনিসের অর্ডার নিশ্চিত, SMS-ও চলে গেল। কোনো লাল লেখা নেই, কোনো crash নেই: ভুলটি নিঃশব্দে গিলে ফেলা হলো।
  5. পুরোটা try/catch-এ মুড়লে? কাজ করে না। বাইরের try শুধু getUser(…) ডাকার মুহূর্তটুকু পাহারা দেয়; callback-গুলো চলে অনেক পরে, নতুন call stack-এ, যখন try ব্লক শেষ। ভেতরে কেউ throw করলে সেটি uncaught exception — ব্রাউজারে console-এ লাল বার্তা, Node.js-এ পুরো process বন্ধ। Callback-এর জগতে ভুল ছুড়ে দেওয়া যায় না, হাতে হাতে পৌঁছে দিতে হয়।

Callback hell-এর সমস্যাগুলো তাহলে এক জায়গায়:

  • পড়া কঠিন: কাজের ক্রম ওপর থেকে নিচে, কিন্তু চোখকে বারবার ডানে-বামে যেতে হয়; শেষের }); }); }); কোনটি কার, বোঝা মুশকিল।
  • ভুল সামলানো ছড়ানো: প্রতিটি স্তরে আলাদা if (err); একটি ভুলে গেলে নিঃশব্দ বাগ। try/catch কাজ করে না।
  • নিয়ন্ত্রণ কঠিন: “তিনবার retry”, “৫ সেকেন্ডে উত্তর না এলে বাতিল”, “এই দুটি একসাথে চালানো” — প্রতিটির জন্য হাতে লেখা জটিল কোড।
  • বিশ্বাস: প্রতিটি স্তরে inversion of control — পাঁচটি লাইব্রেরির যেকোনোটি callback দুবার ডাকলে পুরো শিকল এলোমেলো।

পিরামিড সমতল করা — named function, early return, ছোট module

Promise ছাড়াও callback hell অনেকটা কমানো যায়। callbackhell.com সাইটটির মূল পরামর্শও এগুলোই: nested anonymous function-কে নাম দিয়ে বাইরে আনুন, প্রথমেই ভুল সামলে return করুন (early return), আর সম্পর্কিত function-গুলোকে আলাদা ছোট module-এ রাখুন।

function startOrder(userId) { getUser(userId, onUser); }

function onUser(err, user) {
  if (err) return fail(err);
  getCart(user.id, (err, cart) => onCart(err, user, cart));
}
function onCart(err, user, cart) {
  if (err) return fail(err);
  checkStock(cart.items, (err) => onStock(err, user, cart));
}
function onStock(err, user, cart) {
  if (err) return fail(err);
  placeOrder(user, cart, (err, order) => onOrder(err, user, order));
}
// onOrder → sendSms → showDone, একই ধাঁচে

এখন প্রতিটি ধাপ একটি নাম পাওয়া ছোট function — আলাদা করে পড়া, টেস্ট করা আর stack trace-এ চেনা যায় (anonymous function-এর বদলে onStock নাম দেখা যায়)। কোড আর ডানে সরে না। কিন্তু খেয়াল করুন, যা মেটেনি: প্রতিটি function-এ if (err) এখনো আছে; পরের ধাপে কোন কোন মান লাগবে (user, cart) তা হাতে হাতে পাঠাতে হচ্ছে; আর কাজের পুরো ক্রমটি এক নজরে দেখা যায় না, পাঁচটি function জুড়ে ছড়িয়ে আছে। Callback hell-এর মূল কারণ ইন্ডেন্টেশন নয় — মূল কারণ হলো async ফলের কোনো মান নেই, যা হাতে রেখে পরে ব্যবহার করা যায়। Promise ঠিক সেই মানটি দেয়।

৮ · একসাথে অনেক কাজ — parallel counter, promisify আর টেস্টে timer

পিরামিডের কাজগুলো একটির ওপর আরেকটি নির্ভরশীল, তাই পরপর (series) চালাতে হয়। কিন্তু একটি dashboard খোলার সময় তিনটি জিনিস লাগে — ব্যবহারকারীর তথ্য, cart আর আজকের অফার — যারা একে অপরের ওপর নির্ভর করে না। এদের পরপর চালালে মোট সময় = তিনটির যোগফল; একসাথে (parallel) চালালে মোট সময় = সবচেয়ে ধীরটির সময়। সমস্যা হলো: তিনটি callback যেকোনো ক্রমে ফিরতে পারে, তাহলে “সবগুলো শেষ” বুঝব কীভাবে? উত্তর: একটি counter.

function parallel(tasks, final) {
  const results = [];
  let done = 0, finished = false;
  tasks.forEach((task, i) => {
    task((err, data) => {
      if (finished) return;                      // final একবারই
      if (err) { finished = true; return final(err); }
      results[i] = data;                         // ক্রম বজায় — index দিয়ে
      if (++done === tasks.length) { finished = true; final(null, results); }
    });
  });
}
const fakeRequest = (name, ms, cb) => setTimeout(() => cb(null, `${name}: ঠিক আছে`), ms);
parallel([
  (cb) => fakeRequest("user", 300, cb),
  (cb) => fakeRequest("cart", 100, cb),
  (cb) => fakeRequest("offer", 200, cb),
], (err, all) => console.log(err ? "ব্যর্থ" : all));
// প্রত্যাশিত আউটপুট (৩০০ ms পরে):
// [ 'user: ঠিক আছে', 'cart: ঠিক আছে', 'offer: ঠিক আছে' ]
1tasks.forEach((task, i) => task((err, data) => {2  if (finished) return;3  if (err) { finished = true; return final(err); }4  results[i] = data;5  if (++done === tasks.length) {6    finished = true; final(null, results);7  }8}));
Closure-এ ভাগ করা অবস্থা
সময়
কে ফিরল
done
results
finished
  1. শুরু — তিনটি অনুরোধ একসাথে। forEach তিনটি task-ই সাথে সাথে চালু করে; কেউ কারও জন্য অপেক্ষা করে না। প্রতিটি callback একই closure-এর results, done আর finished দেখে (পর্ব ২২), আর প্রত্যেকে নিজের i মনে রাখে।
  2. ১০০ ms — cart আগে ফিরল। তালিকায় দ্বিতীয় হলেও সবচেয়ে দ্রুত। results[1]-এ রাখা হলো — push নয়, কারণ push করলে ফলের ক্রম হতো ফেরার ক্রম, তালিকার ক্রম নয়। done = ১, এখনো ৩ নয়।
  3. ২০০ ms — offer. results[2], done = ২. এখনো অপেক্ষা।
  4. ৩০০ ms — user, আর counter ৩-এ। results[0] ভরল, ++done হলো ৩ — যা tasks.length-এর সমান। এখন final চলে, ফল তালিকার ক্রমে। মোট সময় ৩০০ ms — পরপর চালালে লাগত ১০০ + ২০০ + ৩০০ = ৬০০ ms.
  5. কেউ ব্যর্থ হলে। ধরুন ২০০ ms-এ offer ভুল দিল। finished = true করে সাথে সাথে final(err). ৩০০ ms-এ user ফিরলে লাইন ২-এর পাহারা তাকে থামায় — নইলে final দ্বিতীয়বার চলত। পর্ব ৩৩-এর Promise.all ঠিক এই আচরণটিই দেয়, এক লাইনে।

২০১০ সালে Caolan McMahon-এর লেখা npm প্যাকেজ async এই ধরনের সহায়ক (async.series, async.parallel, async.waterfall, সীমিত সংখ্যায় একসাথে চালানোর async.parallelLimit) জনপ্রিয় করে, আর Promise আসার আগের যুগে Node.js কোডবেসের প্রায় অপরিহার্য অংশ হয়ে ওঠে। পুরোনো প্রজেক্টে আজও এটি দেখবেন।

util.promisify — callback থেকে Promise-এ সেতু

আজকের নতুন কোড callback-এ লেখা হয় না, কিন্তু callback-ভিত্তিক পুরোনো API সর্বত্র। Node.js 8 (২০১৭) একটি সেতু যোগ করে: util.promisify. এটি যেকোনো error-first callback function নিয়ে একটি Promise-ফেরত-দেওয়া সংস্করণ বানায় — error-first রীতি সব জায়গায় একই বলেই এটি সম্ভব।

const { promisify } = require("node:util");
const fs = require("node:fs");

const readFileP = promisify(fs.readFile);     // (file, enc) => Promise
readFileP("config.json", "utf8")
  .then((text) => console.log(JSON.parse(text).shop))
  .catch((err) => console.log("ভুল:", err.code));

// আজ সরাসরি Promise API-ও আছে:
// require("node:fs/promises").readFile(...)
// require("node:timers/promises").setTimeout(500)  — অপেক্ষার Promise

Promise কীভাবে কাজ করে, then-এর শিকল পিরামিডকে কীভাবে সোজা লাইন বানায়, আর একটি .catch কীভাবে সব স্তরের ভুল এক জায়গায় ধরে — সেটাই পর্ব ৩৩. আর async/await (পর্ব ৩৪) সেই কোডকে প্রায় সাধারণ sync কোডের মতো দেখায়। Debounce আর throttle — timer দিয়ে বানানো আরও দুটি দরকারি নকশা — পর্ব ৩৬-এ।

টেস্টে timer — অপেক্ষা না করে সময় এগিয়ে দেওয়া

OTP-র ৬০ সেকেন্ডের মেয়াদ টেস্ট করতে কি প্রতিবার ৬০ সেকেন্ড অপেক্ষা করবেন? না। টেস্টিং টুলগুলো fake timers দেয়: আসল setTimeout-এর জায়গায় একটি নকল ঘড়ি, যা আপনি নিজে এগিয়ে দেন। Vitest-এ vi.useFakeTimers() চালু করে vi.advanceTimersByTime(60_000) বললে ৬০ সেকেন্ডের সব timer সাথে সাথে চলে যায়; Jest-এ একই কাজ jest.useFakeTimers(), আর Sinon-এর @sinonjs/fake-timers লাইব্রেরি এদের অনেকের ভিত্তি। টেস্ট দ্রুত হয়, আর ফল নিশ্চিত — আসল ঘড়ির এলোমেলো দেরি থাকে না। বিস্তারিত পর্ব ৪৮-এ।

পরীক্ষা ও ইন্টারভিউতে
  • Callback কী, আর sync ও async callback-এর পার্থক্য? — অন্য function-কে দেওয়া function, যা সে পরে ডাকে; sync callback (map, forEach) ফেরত আসার আগেই চলে, async callback (setTimeout, event, I/O) বর্তমান কোড শেষ হওয়ার পরে, event loop-এর মাধ্যমে।
  • setTimeout(fn, 0) কি সাথে সাথে চলে? — না; বর্তমান কোড আর সব microtask শেষ হলে, task queue থেকে। Delay একটি ন্যূনতম সময়।
  • Error-first callback কী, আর error কেন প্রথমে? — (err, data); সফল হলে err = null. প্রথমে থাকায় উপেক্ষা করা কঠিন, আর ফলের সংখ্যা বদলালেও ভুলের জায়গা সব API-তে একই।
  • Callback hell কী, আর সমাধান? — nested async callback-এর পিরামিড, যেখানে পড়া, ভুল সামলানো আর নিয়ন্ত্রণ কঠিন; named function ও early return দিয়ে কমে, আসল সমাধান Promise ও async/await.
  • setInterval-এর বদলে recursive setTimeout কেন? — কাজ interval-এর চেয়ে বেশি সময় নিলে setInterval বিরতি ছাড়া একটানা চলে; recursive setTimeout প্রতিটি কাজের পরে বিরতি নিশ্চিত করে।

৯ · AI-এর যুগে — AI-এর লেখা callback আর timer যাচাই করা

AI মডেল শিখেছে ইন্টারনেটের কোটি লাইন কোড থেকে, যার বিশাল অংশ ২০১০-এর দশকের callback-ভিত্তিক Node.js আর jQuery. তাই callback বা timer-এর কাজ চাইলে চারটি ভুল বারবার দেখা যায়।

১. গভীর পিরামিড। “ফাইল পড়ো, প্রসেস করো, ডেটাবেসে রাখো, ইমেইল পাঠাও” বললে অনেক সময় চার-পাঁচ স্তরের nested callback আসে। ২. ভুলে যাওয়া clearInterval. একটি countdown বা polling শুরু হয়, কিন্তু থামানোর পথ নেই — বা component সরে যাওয়ার সময় পরিষ্কার করা হয় না; ফল memory leak আর ভূতুড়ে আপডেট। ৩. উপেক্ষিত err. (err, data) => { use(data) } — প্রথম parameter লেখা আছে, কিন্তু কখনো পরীক্ষা হয় না। ৪. Callback আর Promise মেশানো। একই function কখনো callback নেয়, কখনো Promise ফেরত দেয়; বা একটি async function-এর ভেতরে callback-এর ভেতরে return লিখে ধরে নেওয়া হয় যে বাইরের function সেটি ফেরত দেবে। এতে ভুল দুবার সামলানো হয়, বা একবারও না।

// ❌ AI-এর একটি সাধারণ ভুল: callback-এর return বাইরের function-এর return নয়
async function getShopName() {
  fs.readFile("config.json", "utf8", (err, text) => {
    return JSON.parse(text).shop;     // এই return যায় callback থেকে, কোথাও না
  });
}                                     // getShopName() → Promise<undefined>
// ✅ ঠিক: const text = await fs.promises.readFile("config.json", "utf8");

দুটি ভুল একসাথে: err দেখা হয়নি, আর callback-এর return শুধু callback থেকে ফেরে — getShopName ততক্ষণে শেষ হয়ে undefined দিয়ে resolve হয়ে গেছে। অংশ ১-এর মূল ধারণা মনে রাখুন: async callback-এর ফল return দিয়ে বাইরে আনা যায় না।

পুরোনো callback কোড আধুনিক করতে বা নতুন timer লেখাতে AI-কে এভাবে বলুন:

নিচের Node.js কোডটি nested callback-এ লেখা (getUser → getCart → checkStock → placeOrder → sendSms). এটি নতুন করে লিখে দাও। নিয়ম: ১. প্রতিটি error-first callback-এ err আগে পরীক্ষা করবে; কোনো ভুল যেন নিঃশব্দে হারিয়ে না যায়। ২. আগে callback রেখেই named function আর early return দিয়ে পিরামিড সমতল করো; তারপর আলাদা সংস্করণে util.promisify আর async/await ব্যবহার করো। দুটি সংস্করণ মেশাবে না। ৩. sendSms ব্যর্থ হলে অর্ডার বাতিল হবে না — শুধু লগ হবে; বাকি যেকোনো ধাপ ব্যর্থ হলে পরের ধাপ চলবে না। ৪. যেকোনো setInterval বা setTimeout-এর id রাখবে আর থামানোর পথ দেখাবে। ৫. কোনো callback যেন দুবার না চলে, আর কোনো callback কখনো sync কখনো async না হয় — কোথায় কীভাবে নিশ্চিত করলে, এক লাইনে বলো। ৬. শেষে বলো: checkStock ভুল দিলে কোন লাইন সেটি ধরবে আর কোন কোন ধাপ চলবে না।

AI-এর লেখা callback বা timer হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Callback কি শুধু JavaScript-এর একটি কৌশল? ১৯৭৫ সালের Scheme আর “continuation-passing style” কীভাবে দেখায় যে প্রতিটি callback আসলে “বাকি প্রোগ্রাম”?

সাধারণ function-এ ফল ফেরত আসে return দিয়ে, আর কলকারী সেখান থেকে বাকি কাজ চালায়। এখন কল্পনা করুন এমন একটি ভাষা, যেখানে কোনো function কখনো return করে না — বরং প্রতিটি function একটি বাড়তি argument নেয়: “ফল পেলে এটিকে দেবেন।” এই বাড়তি function-টি প্রোগ্রামের বাকি অংশকে প্রতিনিধিত্ব করে। তাত্ত্বিক ভাষায় একে বলে continuation, আর এভাবে লেখার রীতির নাম continuation-passing style (CPS).

Continuation-এর ধারণা ১৯৬০-এর দশক থেকেই গবেষকদের কাজে ছিল, কিন্তু “continuation-passing style” নামটি আসে ১৯৭৫ সালে, MIT-র Gerald Jay Sussman আর Guy L. Steele Jr.-এর Scheme নিয়ে লেখা AI Memo থেকে (পর্ব ২২-এর ভাবনার প্রশ্নে এই memo-র কথা এসেছে)। Steele পরে তাঁর Rabbit compiler-এ (১৯৭৮) দেখান যে একটি compiler পুরো প্রোগ্রামকে ভেতরে ভেতরে CPS-এ রূপান্তর করে নিলে অনেক জটিল নিয়ন্ত্রণ — লুপ, exception, ফেরত আসা — একটিমাত্র ধারণায় নেমে আসে: “পরের function-টি ডাকা।” আজও অনেক functional ভাষার compiler-এ CPS একটি মধ্যবর্তী রূপ হিসেবে ব্যবহৃত হয়।

এখন JavaScript-এর দিকে তাকান। fs.readFile(file, (err, text) => { … }) — এখানে callback-টি ঠিক continuation: “ফাইল পড়া শেষ হলে প্রোগ্রামের বাকি অংশ এখান থেকে চলবে।” Node.js-এর async API আসলে হাতে লেখা CPS. আর callback hell কী? CPS-এ লেখা প্রোগ্রাম মানুষের হাতে লিখলে যা হয় — প্রতিটি ধাপের “বাকি প্রোগ্রাম” আগের ধাপের ভেতরে ঢোকে, তাই কোড গভীর থেকে গভীরে যায়। Compiler-এর জন্য এটি চমৎকার মধ্যবর্তী রূপ; মানুষের পড়ার জন্য নয়।

মজার ব্যাপার হলো, ইতিহাস উল্টো দিকে ঘুরেছে। পর্ব ৩৪-এর async/await দিয়ে আপনি সাধারণ, ওপর-থেকে-নিচে কোড লেখেন, আর engine প্রতিটি await-এর পরের অংশকে একটি continuation হিসেবে আলাদা করে রেখে দেয় — অর্থাৎ CPS-এ রূপান্তরের কাজটি মানুষের হাত থেকে আবার যন্ত্রের হাতে ফেরত গেছে, ঠিক Steele-এর compiler যা করেছিল। পর্ব ২৭-এর generator-ও একই ধারণার আত্মীয়: function-কে মাঝপথে থামিয়ে পরে আবার চালু করা।

মূল উপলব্ধি: Callback কোনো JavaScript-বিশেষ কৌশল নয়, বরং continuation-এর একটি ব্যবহারিক রূপ — “বাকি প্রোগ্রাম”-কে মান হিসেবে পাঠানো। CPS যন্ত্রের জন্য শক্তিশালী, মানুষের জন্য ক্লান্তিকর; async/await হলো সেই রূপান্তরের ভার আবার compiler-কে দিয়ে দেওয়া।

প্র ০২Node.js কেন callback বেছে নিল, আর কেন error-first? ২০০৯–২০১০ সালের সিদ্ধান্তটি কী সুবিধা দিয়েছিল, আর কী দাম দিতে হয়েছিল?

২০০৯ সালে Ryan Dahl JSConf EU-তে Node.js পরিচয় করান। তাঁর মূল যুক্তি ছিল: সার্ভারের বেশিরভাগ সময় যায় অপেক্ষায় — ডিস্ক, ডেটাবেস, নেটওয়ার্ক। প্রচলিত সার্ভার প্রতিটি সংযোগের জন্য একটি thread রাখে, আর thread-গুলো বেশিরভাগ সময় বসে থাকে, মেমরি খায়। JavaScript-এ কোনো blocking I/O-র ঐতিহ্য ছিল না, আর ব্রাউজারের কল্যাণে ডেভেলপাররা ইতিমধ্যে event আর callback-এ অভ্যস্ত। তাই Node একটি thread-এ event loop চালিয়ে সব I/O non-blocking করে দেয় — কাজ শুরু করা, callback দেওয়া, পরের অনুরোধে যাওয়া।

একটি কম পরিচিত তথ্য: Node-এর একেবারে শুরুর সংস্করণগুলোতে promise-জাতীয় একটি ব্যবস্থাও ছিল। কিন্তু ২০১০ সালের শুরুতে Dahl সেটি core থেকে সরিয়ে দেন, আর সরল callback-কে মানক রূপ করেন — যুক্তি ছিল, core যত ছোট আর সরল হবে, তার ওপর তত রকম abstraction বানানো যাবে; promise বা অন্য কিছু চাইলে ব্যবহারকারীরা লাইব্রেরি দিয়ে বানিয়ে নেবেন। সেই সময়েই error-first রীতি স্থির হয়, আর npm-এর হাজার হাজার প্যাকেজ এটি মেনে নেয়।

Error কেন প্রথমে? প্রথমত, async জগতে throw কাজ করে না — ভুল কলকারীর stack-এ পৌঁছাতে পারে না, তাই ভুলকেও একটি সাধারণ মান হিসেবে পাঠাতে হয়। দ্বিতীয়ত, প্রথম জায়গায় থাকলে প্রতিটি callback-এর শুরুতেই প্রোগ্রামার ভুলের মুখোমুখি হন; শেষে থাকলে ভুলে যাওয়া সহজ হতো। তৃতীয়ত, ফলের সংখ্যা কাজভেদে আলাদা, কিন্তু ভুল সবসময় index ০-তে — তাই একটি সাধারণ যন্ত্র যেকোনো API-কে বুঝতে পারে। এই নিশ্চয়তার ফলেই ২০১৭ সালে Node 8-এ util.promisify সম্ভব হয়।

দামটিও কম নয়। রীতিটি শুধু একটি রীতি — ভাষা বা runtime কিছু জোর করে না। Callback দুবার ডাকা, err আর ফল দুটোই দেওয়া, ভুল উপেক্ষা করা — সব সম্ভব। আর callback hell-এর জন্ম এখান থেকেই। পরবর্তী বছরগুলোতে Node কমিউনিটিতে “operational error বনাম programmer error” আলাদা করে ভাবার চর্চা গড়ে ওঠে, আর শেষে Node নিজেই fs/promises-এর মতো Promise-ভিত্তিক API যোগ করে।

মূল উপলব্ধি: Callback আর error-first ছিল একটি সচেতন সরলতার সিদ্ধান্ত — ছোট core, এক রীতি, সবার জন্য একই চুক্তি। সেই একরূপতাই পরে Promise-এ যাওয়ার সেতু তৈরি করে দেয়। ভালো রীতির শক্তি হলো, পরে যন্ত্র দিয়ে তাকে বদলানো যায়।

প্র ০৩“callbackhell.com” থেকে Promises/A+ পর্যন্ত — ২০১২ থেকে ২০১৪ সালের মধ্যে JavaScript কমিউনিটি কেন আর কীভাবে callback ছেড়ে Promise-এর দিকে গেল?

২০১০-এর দশকের শুরুতে Node.js দ্রুত জনপ্রিয় হচ্ছিল, আর সাথে বাড়ছিল ক্ষোভ। ছোট ডেমোতে callback চমৎকার, কিন্তু বাস্তব সার্ভারে — যেখানে একটি অনুরোধের জন্য ডেটাবেস, cache, অন্য সার্ভিস আর ফাইল একের পর এক লাগে — কোড ডানদিকে গড়িয়ে যেত। এই সময়ে Node কমিউনিটির পরিচিত মুখ Max Ogden একটি ছোট সাইট লেখেন: callbackhell.com. মজার ব্যাপার, সাইটটি Promise-এর প্রচার করেনি; বরং বলেছিল, কোড অগভীর রাখুন, function-কে নাম দিন, ছোট module বানান, প্রতিটি ভুল সামলান — অর্থাৎ এই পর্বের অংশ ৭-এর পরামর্শ। কিন্তু নামটি টিকে গেল, আর “callback hell” হয়ে উঠল একটি পুরো যুগের সমস্যার ডাকনাম।

এর মধ্যে বিকল্পও তৈরি হচ্ছিল। Promise-এর ধারণা পুরোনো (১৯৭০-এর দশকের গবেষণায় “future” আর “promise” শব্দ পাওয়া যায়)। JavaScript-এ CommonJS গোষ্ঠীর Promises/A প্রস্তাব, Kris Kowal-এর Q লাইব্রেরি আর jQuery 1.5-এর Deferred (২০১১) ধারণাটি ছড়ায়। সমস্যা ছিল, প্রতিটির আচরণ সামান্য আলাদা — বিশেষ করে jQuery-র Deferred কখনো sync-এ callback ডাকত আর then-এর ভেতরের throw ঠিকমতো ধরত না। ২০১২ সালে Domenic Denicola, Brian Cavalier সহ কয়েকজন মিলে একটি ছোট, পরীক্ষাযোগ্য মানদণ্ড লেখেন: Promises/A+. এতে নিশ্চিত করা হয় then-callback সবসময় async (Zalgo নয়), একবারই চলে, আর ভুল শিকল ধরে নিচে গড়ায়। শত শত স্বয়ংক্রিয় টেস্ট দিয়ে যেকোনো লাইব্রেরি যাচাই করা যেত।

২০১৩ সালে Petka Antonov-এর Bluebird দেখায় যে Promise দ্রুতও হতে পারে। শেষে ES2015 ভাষার ভেতরেই Promise যোগ করে, Promises/A+-এর আচরণ মেনে। তারপর ES2017-এ async/await আসে, আর Node.js 8 আনে util.promisify. কয়েক বছরের মধ্যে নতুন API-গুলো (যেমন ব্রাউজারের fetch) সরাসরি Promise ফেরত দিতে শুরু করে।

এই পরিবর্তন মসৃণ ছিল না। Node core-এ Promise আনা নিয়ে দীর্ঘ বিতর্ক হয়েছিল — কেউ বলতেন callback দ্রুত আর সরল, কেউ বলতেন Promise ভুল সামলানো নিরাপদ করে। শেষে বাস্তব অভিজ্ঞতা জয়ী হয়: বড় কোডবেসে ভুল সামলানো আর গঠন অনেক বেশি গুরুত্বপূর্ণ প্রমাণিত হয়।

মূল উপলব্ধি: একটি কমিউনিটি কীভাবে নিজের যন্ত্র বদলায়, তার চমৎকার উদাহরণ — প্রথমে সমস্যার একটি নাম (callback hell), তারপর প্রতিযোগী সমাধান, তারপর একটি পরীক্ষাযোগ্য মানদণ্ড (Promises/A+), শেষে ভাষার অংশ। Promise শেখার সময় মনে রাখুন, এর প্রতিটি নিয়ম একটি বাস্তব callback-বাগ থেকে জন্মেছে।

প্র ০৪Kyle Simpson তাঁর “You Don't Know JS: Async & Performance” বইয়ে callback-এর আসল সমস্যাকে “বিশ্বাসের সমস্যা” বলেছেন। Inversion of control কেন শুধু সুন্দর কোডের নয়, নিরাপত্তা আর সঠিকতার প্রশ্ন?

বেশিরভাগ মানুষ callback-এর সমস্যা বলতে বোঝেন ইন্ডেন্টেশন — ডানদিকে গড়ানো কোড। Kyle Simpson তাঁর জনপ্রিয় বই-সিরিজ “You Don't Know JS”-এর “Async & Performance” খণ্ডে যুক্তি দেন যে এটি আসল সমস্যা নয়। আসল সমস্যা দুটি: আমাদের মস্তিষ্ক ধাপে ধাপে, ক্রমিকভাবে পরিকল্পনা করে, অথচ callback কোড সেই ক্রমকে টুকরো টুকরো করে বিভিন্ন জায়গায় ছড়িয়ে দেয়; আর দ্বিতীয়টি আরও গুরুতর — বিশ্বাস.

তাঁর উদাহরণটি এরকম: একটি ই-কমার্স সাইটে “কিনুন” চাপলে একটি third-party analytics লাইব্রেরিকে callback দেওয়া হয়, আর সেই callback-এর ভেতরে গ্রাহকের কার্ড চার্জ হয়। একদিন লাইব্রেরির একটি আপডেটে retry-এর বাগে callback পাঁচবার ডাকা হয় — গ্রাহক পাঁচবার চার্জ হন। আপনার কোডে কোনো ভুল ছিল না; ভুল ছিল এই অনুমানে যে অন্যের কোড আপনার callback ঠিক একবার, ঠিক সময়ে ডাকবে। Simpson এই অনুমানগুলোর একটি তালিকা দেন: খুব তাড়াতাড়ি ডাকা, খুব দেরিতে বা কখনোই না, খুব কম বা খুব বেশিবার, প্রয়োজনীয় argument না দেওয়া, আর ভুল চুপচাপ গিলে ফেলা।

বাংলাদেশি উদাহরণে ভাবুন: আপনি একটি কুরিয়ার সার্ভিসকে বললেন, “পার্সেল পৌঁছালে আমার দোকানের কর্মচারীকে বলবেন টাকা ফেরত দিতে।” এখন টাকা ফেরতের সিদ্ধান্ত কুরিয়ারের হাতে — তারা দুবার বললে কর্মচারী দুবার টাকা দেবেন। এটাই inversion of control: সিদ্ধান্তের চাবি আপনার হাত থেকে অন্যের হাতে। অংশ ৬-এর guarded wrapper ঠিক এই কর্মচারীকে শিখিয়ে দেওয়া: “একবারের বেশি শুনবেন না, আর এত সময়ের মধ্যে খবর না এলে নিজেই ধরে নেবেন কিছু গোলমাল।”

Simpson-এর যুক্তি হলো, Promise এই নিয়ন্ত্রণ আপনার হাতে ফিরিয়ে দেয়। Callback দিলে আপনি অন্যকে আপনার “বাকি প্রোগ্রাম” দেন। Promise-এ উল্টো: অন্যজন আপনাকে একটি promise ফেরত দেয় — ভবিষ্যৎ ফলের একটি মান — আর আপনি ঠিক করেন সেটি দিয়ে কী করবেন। Promise একবারই settle হয়, then-callback সবসময় async চলে, আর ভেতরের ভুল rejection হয়ে আসে — Simpson-এর তালিকার বেশিরভাগ ঝুঁকি মানদণ্ডের স্তরে বন্ধ। “কখনো না” সমস্যাটির জন্য Promise.race দিয়ে timeout বসানো যায় (পর্ব ৩৩)।

মূল উপলব্ধি: Callback দেওয়া মানে নিজের প্রোগ্রামের একটি অংশ চালানোর অধিকার অন্যকে দেওয়া। ভালো ডেভেলপার প্রশ্ন করেন: “এই কোড আমার callback ভুলভাবে ডাকলে কী হবে?” Promise-এর আসল উপহার সুন্দর syntax নয় — বিশ্বাসযোগ্য নিশ্চয়তা।

প্র ০৫Isaac Schlueter-এর “Designing APIs for Asynchrony” (২০১৩) কেন বলে “Zalgo-কে মুক্ত করবেন না”? একটি API সবসময় sync বা সবসময় async হওয়া কেন এত জরুরি, এমনকি গতি কিছুটা কমলেও?

Isaac Z. Schlueter npm-এর স্রষ্টা, আর Ryan Dahl-এর পরে প্রায় দুই বছর (২০১২–২০১৪) Node.js প্রকল্পের নেতৃত্ব দেন। ২০১৩ সালে তিনি তাঁর ব্লগে “Designing APIs for Asynchrony” নামে একটি ছোট লেখা প্রকাশ করেন, যা পরে Node কমিউনিটির একটি ক্লাসিক হয়ে যায়। লেখার মূল বক্তব্য একটি বাক্যে: যে function কখনো callback async-এ ডাকে, সেটি সবসময় async-এ ডাকবে। নিয়ম ভাঙাকে তিনি নাম দেন “releasing Zalgo” — ইন্টারনেটের একটি ভৌতিক মিম থেকে, যেখানে Zalgo হলো এমন এক বিশৃঙ্খল শক্তি যে মুক্তি পেলে সব কিছু ভেঙে পড়ে। একই ধারণা আগে (২০১১ সালে) Havoc Pennington-ও একটি লেখায় তুলে ধরেছিলেন।

কেন এত জোর? কারণ কলকারী কোড লেখার সময় দুটি সম্ভাবনার একটির ওপর নির্ভর করে: callback তার পরের লাইনের আগে চলবে, নাকি পরে। অংশ ৬-এর spinner উদাহরণে দেখেছেন, একই function দুই রকম আচরণ করলে বাগটি দেখা দেয় শুধু কিছু অবস্থায় — যেমন cache-এ জিনিসটি থাকলে। এমন বাগ টেস্টে প্রায়ই ধরা পড়ে না, production-এ মাঝে মাঝে দেখা দেয়, আর আবার তৈরি করা কঠিন। আরও সূক্ষ্ম বিপদ: sync পথে callback যদি throw করে, ভুলটি API-এর নিজের কোডের ভেতর দিয়ে উঠে আসে, আর API-এর অর্ধেক-করা কাজ বা ভাঙা অবস্থা রেখে যেতে পারে।

Schlueter লেখেন, গতির জন্য sync পথ রাখার লোভ বোঝা যায় — cache hit-এ কেন অপেক্ষা? কিন্তু একটি microtask বা process.nextTick-এর খরচ খুব সামান্য, আর বিনিময়ে পাওয়া যায় নিশ্চিত, যুক্তিযোগ্য আচরণ। সত্যিই sync গতি দরকার হলে আলাদা function দিন — যেমন Node-এর fs.readFileSync আর fs.readFile, নাম দেখেই আচরণ বোঝা যায়। তিনি পরে dezalgo নামে একটি ছোট npm প্যাকেজও প্রকাশ করেন, যা একটি callback-কে মুড়ে দেয় যাতে সেটি কখনো sync-এ না চলে।

এই নীতি পরে ভাষার মানদণ্ডে ঢুকে যায়। Promises/A+ আর ES2015 Promise নিশ্চিত করে যে promise আগে থেকেই resolved থাকলেও then-এর callback সবসময় পরে, microtask হিসেবে চলে। তাই Promise.resolve(5).then(log); console.log("আগে") সবসময় আগে “আগে” ছাপে। পর্ব ৩১-এর microtask queue এই নিশ্চয়তার যন্ত্র।

মূল উপলব্ধি: একটি API-এর গতির চেয়ে তার আচরণের পূর্বানুমেয়তা বেশি মূল্যবান। “সবসময় sync” বা “সবসময় async” — দুটোই ঠিক; “কখনো এটা, কখনো ওটা” কখনো নয়। নিজের API লেখার সময় এই একটি নিয়ম মানলে এক শ্রেণির রহস্যময় বাগ পুরোপুরি এড়ানো যায়।

অনুশীলন

এগুলোর বেশিরভাগে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর console-এ চালিয়ে মিলিয়ে নিন।

  1. আউটপুট অনুমান করুন: console.log("1"); setTimeout(() => console.log("2"), 0); [7, 8].forEach((x) => console.log("f" + x)); setTimeout(() => console.log("3"), 0); console.log("4");

    1, f7, f8, 4, 2, 3. forEach-এর callback sync, তাই সাথে সাথে চলে। দুটি setTimeout-এর callback task queue-তে যায় আর script শেষ হলে চলে — একই delay হলে যে ক্রমে বসানো হয়েছে সেই ক্রমে, তাই আগে 2, তারপর 3.

  2. বাগ খুঁজুন: একটি দোকানের সাইটে “অফার শেষ হতে বাকি” দেখানোর কোড: let s = 60; setInterval(function () { s--; this.label.textContent = s; }, 1000); — তিনটি সমস্যা খুঁজে বের করুন।

    (১) this হারিয়েছে: সাধারণ function-কে timer ডাকে object ছাড়া, তাই this.label কাজ করবে না — arrow function আর বাইরের কোনো variable-এ রাখা element ব্যবহার করুন। (২) Id রাখা হয়নি আর থামার শর্ত নেই — ০-এর পরে −১, −২… চলতেই থাকবে; const id = setInterval(…) আর if (s <= 0) clearInterval(id). (৩) Tick গুনে সময় মাপা হচ্ছে — পেছনের ট্যাবে বা ব্যস্ত পাতায় দেখানো সময় ভুল হবে (drift); শেষ সময় Date.now() + 60_000 রেখে প্রতি tick-এ বাকি সময় হিসাব করুন।

  3. ভাবুন: একটি লাইব্রেরি loadAvatar(url, cb) দেয়, যা ছবি cache-এ থাকলে cb সাথে সাথে ডাকে, না থাকলে ডাউনলোড শেষে। আপনার কোড: loadAvatar(u, (img) => box.append(img)); box = document.querySelector("#avatar"); — কখন কাজ করবে, কখন করবে না? এই সমস্যার নাম কী, আর লাইব্রেরির লেখক কীভাবে ঠিক করবেন?

    Cache miss-এ কাজ করবে: callback পরে চলে, ততক্ষণে box বসানো হয়ে গেছে। Cache hit-এ ভাঙবে: callback সাথে সাথে চলে, তখনো box-এ পুরোনো মান (হয়তো undefined), তাই TypeError. এটাই Zalgo — callback কখনো sync, কখনো async. লাইব্রেরির লেখক cache hit-এও callback-কে queueMicrotask (বা Promise) দিয়ে পরে পাঠাবেন, যাতে সবসময় async হয়। আপনার দিক থেকেও ভালো অভ্যাস: callback-এর ব্যবহার করা সবকিছু কল করার আগেই প্রস্তুত রাখুন।

  4. কোড লিখুন — series: অংশ ৮-এর parallel-এর মতো একটি series(tasks, final) লিখুন: task-গুলো একটির পর একটি চলবে (আগেরটি শেষ হলে পরেরটি), কোনোটি ভুল দিলে সাথে সাথে final(err), সব সফল হলে final(null, results). Nested callback ছাড়াই।

    একটি সম্ভাব্য সমাধান — recursive setTimeout-এর মতোই “শেষ হলে পরেরটি” কৌশল:

    function series(tasks, final) {
      const results = [];
      function next(i) {
        if (i === tasks.length) return final(null, results);
        tasks[i]((err, data) => {
          if (err) return final(err);      // ভুল হলে বাকিগুলো চলবে না
          results.push(data);              // পরপর চলে, তাই push-এ ক্রম ঠিক
          next(i + 1);
        });
      }
      next(0);
    }

    পিরামিড নেই, কারণ “পরের ধাপ” প্রতিবার একই function next — শুধু index বদলায়। এখানে push নিরাপদ, কারণ task-গুলো একসাথে চলে না। পুরোপুরি নিরাপদ করতে চাইলে প্রতিটি task-এর callback-কে once দিয়ে মুড়তে পারেন, যাতে কোনো task দুবার callback ডাকলেও পরের ধাপ দুবার শুরু না হয়।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমব্রাউজারে setTimeout(cb, 100) লেখার পর থেকে cb চলা পর্যন্ত ঘটনাগুলো কোন ক্রমে ঘটে, সাজান (▲▼ চেপে বা টেনে)।

  1. ⏱️ setTimeout timer রেজিস্টার করে একটি id ফেরত দেয়
  2. ➡️ script-এর বাকি sync কোড চলতে থাকে আর শেষ হয়
  3. ⌛ অন্তত ১০০ ms পেরোয়, ব্রাউজার cb-কে task queue-তে রাখে
  4. 🔄 call stack খালি হলে event loop cb-কে queue থেকে নেয়
  5. ▶️ cb চলে

JavaScript-এ setTimeout(fn, 1000) কী নিশ্চিত করে?

  • fn ঠিক ১০০০ ms পরে চলবে
  • ১০০০ ms ধরে বাকি কোড থেমে থাকবে
  • fn অন্তত ১০০০ ms পরে চলবে, দেরিও হতে পারে
  • fn প্রতি ১০০০ ms-এ বারবার চলবে

console.log("A"); setTimeout(() => console.log("B"), 0); console.log("C"); — কী ছাপা হয়?

  • A B C
  • A C B
  • B A C
  • A C (B কখনো ছাপা হয় না)

Node.js-এর error-first callback-এ কাজ সফল হলে প্রথম argument কী থাকে?

  • null
  • কাজের ফল
  • একটি খালি Error object
  • true

একটি callback-ভিত্তিক API কখনো callback সাথে সাথে (sync) ডাকে, কখনো পরে (async) — এই সমস্যাকে কী বলে?

  • Callback hell
  • Timer drift
  • Memory leak
  • Zalgo

Callback-এর কাজে interval-এর চেয়ে বেশি সময় লাগতে পারলে setInterval-এর বদলে কোনটি ভালো?

  • আরও ছোট interval-এর setInterval
  • Callback-এর শেষে পরের setTimeout বসানো (recursive setTimeout)
  • while লুপে Date.now() দেখে অপেক্ষা
  • একই কাজের জন্য দুটি setInterval

Callback প্রসঙ্গে “inversion of control” বলতে কী বোঝায়?

  • Callback-গুলো উল্টো ক্রমে চলা
  • Callback-কে Promise-এ রূপান্তর করা
  • আপনার callback কখন ও কতবার চলবে, সেই নিয়ন্ত্রণ অন্যের কোডের হাতে তুলে দেওয়া
  • Timer-এ this-এর মান হারিয়ে যাওয়া

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখতে পাবেন।

মনে রাখুন

  1. Async callback = “কাজ শেষ হলে ডাকুন।” Sync callback (map, forEach) এখনই চলে; async callback (timer, event, I/O) বর্তমান কোড শেষ হলে, event loop-এর মাধ্যমে। এর ফল return দিয়ে বাইরে আসে না।
  2. Timer-এর delay ন্যূনতম সময়। id রাখুন আর clearTimeout/clearInterval দিয়ে থামান। দীর্ঘ কাজে recursive setTimeout; countdown শেষ সময় থেকে হিসাব করুন, tick গুনে নয়।
  3. Error-first: (err, data). প্রথম লাইনেই if (err) return …; async callback-এর ভুল try/catch ধরে না, হাতে হাতে পৌঁছাতে হয়।
  4. Zalgo আর inversion of control: callback সবসময় async রাখুন (queueMicrotask), আর অন্যের কোড আপনার callback দুবার বা কখনো না ডাকলে কী হবে — once আর timeout দিয়ে পাহারা দিন।
  5. Callback hell: পরপর async ধাপ = পিরামিড, ছড়ানো ভুল সামলানো, কঠিন নিয়ন্ত্রণ। Named function আর early return দিয়ে কমান; parallel কাজে counter; আর util.promisify দিয়ে Promise-এর দিকে যান।

পরের পর্বে: এই পর্বের প্রতিটি সমস্যার উত্তর — Promise. ভবিষ্যৎ ফলকে একটি মান হিসেবে হাতে রাখা, then-এর শিকলে পিরামিডকে সোজা লাইন বানানো, এক .catch-এ সব ভুল ধরা, আর Promise.all, allSettled, race, any দিয়ে এই পর্বের parallel counter এক লাইনে লেখা।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান