Learn JavaScript

lesson ১ / ৯ · async 2: promise, async/await আর fetch

Module ১৩ · async 2: promise, async/await আর fetch

Promise-এর ছবি: পরে আসবে এমন value-এর একটা token

Freeপড়া

এই lesson-এ যা শিখবে

  • বুঝিয়ে বলো, যে value পরে আসবে সেটা একটা function সরাসরি return করতে পারে না কেন, আর তার বদলে promise কী ফেরত দেয়।
  • Promise-এর তিনটা অবস্থার নাম বলো, আর বলো একবার settle হওয়া promise আর কখনো বদলায় না কেন।
  • then দিয়ে promise থেকে value পড়ো, catch দিয়ে ব্যর্থতা সামলাও, আর await দিয়ে অপেক্ষা করো।

পুরান ঢাকার এক ভিড়ের দোকানে Lima কাচ্চি অর্ডার দিল। বাবুর্চি তখনই প্লেট হাতে ধরিয়ে দেয় না। কাউন্টারের লোকটা ওকে একটা নম্বর লেখা token দেয়। Token তো আর খাবার না। এটা একটা কথা দেওয়া: খাবার আসবে, আর ততক্ষণ কোথায় দাঁড়াতে হবে সেটাও বলে দেয়।

Token হাতে রেখে Lima অন্য কাজ করতে পারে। একটা drink কিনতে পারে, বন্ধুকে phone দিতে পারে। নম্বর ডাকলে token দিয়ে প্লেট নিয়ে নেয়। আর রান্নাঘরে চাল শেষ হয়ে গেলে, token-এর বদলে আসে একটা "দুঃখিত"।

JavaScript-এ হুবহু এই token আছে। নাম promise। Module 12-এ তুমি event loop আর callback দেখেছ, দেখেছ callback কীভাবে একটার ভেতরে আরেকটা ঢুকে পিরামিড বানায়। Module 12 যে মুক্তির কথা দিয়েছিল, এই module সেটাই। প্রথমে token-টা বুঝবে, তারপর ওর প্রতিটা method, তারপর async আর await, যেগুলো অপেক্ষার code-কে সাধারণ code-এর মতো পড়তে দেয়।

একটা value, যেটা এখনো আসেনি

Promise কোন সমস্যার সমাধান, আগে সেটা দেখি। একটা function ধীর কিছু চায়: server থেকে একটা দাম, disk থেকে একটা file। ও উত্তরটা return করতে চায়। কিন্তু উত্তর আসে function শেষ হয়ে যাওয়ার পরে।

function getPrice() {
  let price;
  setTimeout(() => {
    price = 120;
  }, 10);
  return price;
}

console.log(getPrice());
console.log("the function has already returned");
undefined
the function has already returned
Run in Compiler

Function-টা সাথে সাথেই return করে, timer বাজার আগেই। তাই return price চলার সময় price ফাঁকা, আর program print করে undefined। 120 আসে ঠিকই, দশ millisecond পরে, কিন্তু তখন আর কেউ শুনছে না।

Module 12-এর উত্তর ছিল callback: একটা function দিয়ে দাও, value এলে সেটাকে ডাকা হবে। এক ধাপের জন্য এটা চলে। পরপর তিনটা ধীর ধাপ থাকলে প্রতিটা callback আগেরটার ভেতরে বসে যায়। Promise উল্টো দিক থেকে সমাধান করে। Function তখনই কিছু একটা ফেরত দেয়: value-এর একটা token।

Promise মানে পরে আসা value-এর একটা token

Promise এমন একটা object, যেটা এমন একটা value-এর হয়ে দাঁড়ায় যেটা হয়তো এখনো নেই। বানাও new Promise দিয়ে। ওকে একটা function দাও, যার নাম executor। JavaScript সেটাকে সাথে সাথেই ডাকে, হাতে দুটো বোতাম দিয়ে: resolve আর reject।

const token = new Promise((resolve) => {
  setTimeout(() => resolve("kacchi"), 10);
});

console.log(token);
token.then((dish) => {
  console.log("got " + dish);
  console.log(token);
});
Promise { <pending> }
got kacchi
Promise { 'kacchi' }
Run in Compiler

প্রথম console.log(token) timer বাজার আগের promise-টা দেখায়। Node ওটাকে print করে Promise { <pending> } হিসেবে: token আছে, খাবার নেই। Timer যখন resolve("kacchi") ডাকে, promise ভরে যায়। তখন then-এর callback চলে, আর দ্বিতীয় print ভেতরের value-টা দেখায়।

resolve চাপা মানে ধীর code বলছে "এই নাও value"। reject চাপা মানে "হয়নি, আর এই তার কারণ"। কোনটা চাপবে আর কখন, সেটা ঠিক করে executor।

তিনটা অবস্থা, settle হয় একবারই

একটা promise সবসময় এই তিনটা অবস্থার যেকোনো একটায় থাকে:

  • pending: value এখনো আসেনি। প্রতিটা promise এখান থেকেই শুরু করে।
  • fulfilled: resolve ডাকা হয়েছে, promise এখন value ধরে আছে।
  • rejected: reject ডাকা হয়েছে, promise এখন একটা কারণ ধরে আছে, সাধারণত একটা Error।

Fulfilled আর rejected মিলিয়ে বলে settled। Promise-কে নিরাপদ বানায় একটা ছোট নিয়ম: promise settle হয় একবারই। এরপর যত resolve বা reject আসুক, সব উপেক্ষা করা হয়।

const p = new Promise((resolve, reject) => {
  resolve("first");
  resolve("second");
  reject(new Error("too late"));
});

p.then((value) => console.log("value:", value));
p.then((value) => console.log("again:", value));
value: first
again: first
Run in Compiler

গোনা হয় শুধু "first"। দ্বিতীয় resolve আর reject কিছুই বদলায় না। আর আলাদা দুটো then একই value পায়, কারণ promise value-টা রেখে দেয়। Token একবার প্লেটে বদলালে প্লেটই থাকে। Callback এই কথা দিতে পারত না: গোলমেলে কোনো library তোমাকে দুবার ডেকে ফেলতে পারত। Module 12-এর bug gallery এটা দেখিয়েছে।

then: value এলে এটা চালাও

then দিয়ে তুমি বলো "এই promise ভরলে এই function-টা value দিয়ে চালিও"। যে function দাও সেটা একটা callback, কিন্তু সেটা ধীর code-এর হাতে না, token-এর গায়ে লাগানো। তার ওপর then ফেরত দেয় একটা নতুন promise, তাই পরের ধাপটা এর গায়ে জুড়ে দেওয়া যায়।

const cook = (dish, ms) => new Promise((resolve) => setTimeout(() => resolve(dish), ms));

cook("kacchi", 10)
  .then((dish) => dish + " with borhani")
  .then((meal) => meal.toUpperCase())
  .then((text) => console.log(text));

console.log("the counter keeps serving");
the counter keeps serving
KACCHI WITH BORHANI
Run in Compiler

প্রতিটা then পায় আগের ধাপ যা return করেছে। প্রথমটা একটা drink যোগ করে, দ্বিতীয়টা পুরোটা বড় হাতের অক্ষরে বানায়, তৃতীয়টা print করে। Output-এর দুটো লাইনের ক্রম খেয়াল করো। File-এর শেষ লাইনটা আগে print হয়, কারণ chain চলে শুধু timer-এর পরে, loop-এর পরের কোনো পালায়। File থেমে অপেক্ষা করেনি।

Module 12-এর পিরামিডের সাথে মিলিয়ে দেখো। তিনটা ধাপ মানে তিনটা সমান লাইন, একটার নিচে আরেকটা, তিন তলা ভেতরে ঢোকা না।

catch: ব্যর্থ হলে এটা চালাও

Promise rejected হলে then-এর callback-গুলো বাদ পড়ে। Rejection chain বেয়ে নিচে নামতে থাকে, যতক্ষণ না একটা catch পায়। catch হলো শুধু ব্যর্থতার রাস্তার জন্য একটা then।

const cook = (dish, ms) =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      if (dish === "kacchi") reject(new Error("rice is finished"));
      else resolve(dish);
    }, ms);
  });

cook("kacchi", 10)
  .then((dish) => console.log("eating " + dish))
  .catch((err) => console.log("sorry: " + err.message))
  .then(() => console.log("Lima goes home"));
sorry: rice is finished
Lima goes home
Run in Compiler

যে then খাওয়ার কথা বলত, সেটা বাদ যায়, catch চলে error নিয়ে, আর chain তার পরে আবার এগোয়। যে catch স্বাভাবিকভাবে return করে, সে chain-কে আবার fulfilled বানিয়ে দেয়। এই কারণেই শেষ লাইনটাও চলে। এই প্রবাহের সব নিয়ম Lesson 02-এ আছে।

await: async function-এর ভেতরে অপেক্ষা

Chain সমান ঠিকই, কিন্তু তবু একটু পাশ দিয়ে পড়তে হয়। JavaScript-এ একই জিনিসের আরেকটা লেখার ধরন আছে। async চিহ্ন দেওয়া function-এর ভেতরে একটা promise-এর আগে await লেখা যায়। Promise settle না হওয়া পর্যন্ত function ওই লাইনে থেমে থাকে। তারপর value নিয়ে এগোয়, যেন value-টা শুরু থেকেই হাতে ছিল।

আসল কথা হলো থামাটা। থামে শুধু এই একটা function। Program-এর বাকিটা চলতে থাকে, loop-ও পালা নিতে থাকে। Promise settle হলে function ঠিক যেখানে থেমেছিল, সেখান থেকে আবার ধরে।

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function countdown() {
  for (let n = 3; n >= 1; n--) {
    console.log(n);
    await sleep(10);
  }
  console.log("go");
}

countdown();
console.log("countdown started");
3
countdown started
2
1
go
Run in Compiler

sleep হলো সবচেয়ে ছোট কাজের promise: ms millisecond পরে ভরে, কোনো value ছাড়া। countdown-এর ভেতরে loop ওপর থেকে নিচে পড়া যায়, কোথাও কোনো callback নেই। কিন্তু দেখো "countdown started" কোথায় এসে পড়েছে। Function প্রথম await পর্যন্ত চলেছে, থেমেছে, আর নিয়ন্ত্রণ caller-কে ফেরত দিয়েছে। Countdown যখন ঘুমাচ্ছিল, তখন caller-এর পরের লাইন চলেছে।

নিচের widget-এ একটা ছোট chain দেখো, এক এক ধাপ করে। একদিকে promise-গুলো আর তাদের অবস্থা, আরেকদিকে await-এ থেমে থাকা function-গুলো, আর নিচে print হওয়া লাইনগুলো।

একই ধাপগুলো table-এ, প্রতি ধাপে একটা row।

ধাপLineWaitingOutputকী হলো
1const order = cook("khichuri", 30);order এখন pending: timer চলছে, খাবার এখনো তৈরি হয়নি।
2const packed = order.then((dish) => cook(dish + " in a box", 20));then ফেরত দেয় নতুন একটা promise, packed, সেটাও pending।
3rider();riderrider চলে await packed পর্যন্ত, তারপর ওখানে থেমে থাকে।
4log("next customer");ridernext customerrider অপেক্ষায়, কিন্তু file-এর বাকিটা চলছে; packed এখনো pending।
5// a later turn of the loopridernext customerorder এখন fulfilled, ভেতরে 'khichuri'।
6// a later turn of the loopridernext customerpacked এখন fulfilled, ভেতরে 'khichuri in a box'।
7log("deliver " + parcel);next customer deliver khichuri in a boxawait-এর পরে rider আবার চলে, নিজের লাইনটা print করে।

মনে রাখার ধাপটা হলো ধাপ 3: rider থেমে আছে, তবু ধাপ 4 print হয়। ধাপ 5 আর 6 ঘটে পরের পালাগুলোয়, timer বাজলে। packed ভরার পরেই কেবল rider জেগে ওঠে আর print করে।

এক নজরে promise

const p = new Promise((resolve, reject) => { ... });  // the executor runs at once
resolve(value)        reject(new Error("why"))      // settle it, once
p.then((value) => next)                             // runs when p is fulfilled; returns a new promise
p.catch((err) => fallback)                          // runs when p is rejected
async function f() {                                // f always returns a promise
  const value = await p;                            // pause f until p settles
}
Example 1: Module 12-এর পিরামিড, সমান করে

Module 12-এ অপেক্ষা করা তিনটা ধাপের জন্য লেগেছিল তিন তলা callback। এখানে সেই তিনটা ধাপই promise। প্রতিটা ধাপ একটা function, যেটা একটা promise ফেরত দেয়, আর chain সেগুলোকে ক্রমমতো সাজায়।

const step = (name, ms) =>
  new Promise((resolve) => setTimeout(() => resolve(name), ms));

step("wash rice", 10)
  .then((done) => {
    console.log(done);
    return step("cook rice", 20);
  })
  .then((done) => {
    console.log(done);
    return step("add mutton", 10);
  })
  .then((done) => console.log(done + ", ready"));
wash rice
cook rice
add mutton, ready

প্রতিটা callback পরের promise-টা return করে, আর পরের then চলার আগে chain সেটার জন্য অপেক্ষা করে। পুরো কৌশলটাই এই return। ওটা না থাকলে পরের then সাথে সাথেই undefined নিয়ে চলে যেত। Lesson 06-এর bug gallery এই bug-টা দেখায়।

Run in Compiler
Example 2: একই ধাপ, await দিয়ে

এই সেই তিনটা ধাপ, এবার async আর await দিয়ে লেখা। Function-টা পড়তে লাগে রান্নার একটা রেসিপির মতো, ওপর থেকে নিচে।

const step = (name, ms) =>
  new Promise((resolve) => setTimeout(() => resolve(name), ms));

async function cookKacchi() {
  console.log(await step("wash rice", 10));
  console.log(await step("cook rice", 20));
  const last = await step("add mutton", 10);
  return last + ", ready";
}

cookKacchi().then((text) => console.log(text));
console.log("the kitchen is busy");
the kitchen is busy
wash rice
cook rice
add mutton, ready

async function-এর ভেতরের return সেই promise-টা ভরে, যেটা function ফেরত দেয়। তাই caller শেষ লাইনটা then দিয়ে পড়ে। প্রতিটা async function একটা promise ফেরত দেয়, সবসময়। একটা throw করলে কী হয়, Lesson 02 দেখায়।

Run in Compiler

এটা কোথায় কাজে লাগে

  • Node.js: fs/promises module file-এর function-গুলোর promise সংস্করণ দেয়, তাই await readFile("a.txt", "utf8") কোনো callback ছাড়াই file পড়ে।
  • Web browser: fetch(url) server-এর উত্তরের জন্য একটা promise ফেরত দেয়, Lesson 03 এটা ব্যবহার করে।
  • Express আর Fastify: route handler একটা async function হতে পারে, আর framework উত্তর পাঠানোর আগে ওর promise-এর জন্য অপেক্ষা করে।
  • Playwright: test-এ browser-এর প্রতিটা কাজ, যেমন await page.click("text=Login"), একটা promise, যেটা তুমি await করো।

যে ভুলগুলো সবাই করে

১. Value-এর বদলে promise-টাই print করা

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const price = sleep(10).then(() => 120);
console.log(price);
console.log(price + 30);
Promise { <pending> }
[object Promise]30

প্রথম print দেখায় একটা pending promise, 120 না। দ্বিতীয়টা আরো খারাপ: promise-এর সাথে সংখ্যা যোগ করলে ওটা হয়ে যায় [object Promise] লেখাটা। Value থাকে token-এর ভেতরে। পড়ো price.then(...) দিয়ে, অথবা async function-এর ভেতরে await price দিয়ে।

২. async function-এর বাইরে await

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function wait() {
  await sleep(10);
  console.log("done");
}
wait();

Node file-টা নেয় না, বলে SyntaxError: await is only valid in async functions and the top level bodies of modules। await কাজ করে শুধু async চিহ্ন দেওয়া function-এর ভেতরে। Playground-এর main.js-এর মতো CommonJS file-এর top level-ও তেমন জায়গা না। async function wait() লেখো, file চলবে।

৩. যে executor কখনো settle করে না

const ready = new Promise((resolve) => {
  console.log("cooking");
});

ready.then(() => console.log("served"));
console.log("waiting at the counter");
cooking
waiting at the counter

"served" কখনো print হয় না। Executor কখনো resolve ডাকে না, তাই promise চিরকাল pending থাকে, আর ওর then কখনো চলে না। Node তোমাকে কোনো সতর্কও করে না। করার কিছু বাকি না থাকলে চুপচাপ বেরিয়ে যায়। Executor-এর প্রতিটা রাস্তায় resolve বা reject চাপতেই হবে।

মাথা খাটাও

কোন লাইন আগে?

Zara সাধারণ লাইনের সাথে মেশায় এমন একটা promise, যেটা আগে থেকেই fulfilled। ওর দাবি, then-ই আগে চলবে, কারণ কাউকে তো অপেক্ষা করতে হচ্ছে না।

const ready = Promise.resolve("tea");
ready.then((drink) => console.log("2: " + drink));
console.log("1: order taken");

ও কি ঠিক? একটা then callback কখন চলার অনুমতি পায়, সেটা ভাবো।

Exercise 1সহজ

Rafi ক্লাসের জন্য একটা ধীরে পড়ার program চায়। Program একটা বাক্যের শব্দগুলো একটা একটা করে print করে, মাঝে একটু বিরতি দিয়ে, তারপর print করে done।

Input. n, তারপর millisecond-এ বিরতি, তারপর n টা শব্দ।

Output. প্রতিটা শব্দ নিজের লাইনে, যে ক্রমে দেওয়া আছে, তারপর done।

Constraints. 1 <= n <= 10। বিরতি 1 থেকে 20। প্রতিটা শব্দ 1 থেকে 10 অক্ষরের।

Sample. নিচের input।

3 15
amar sonar bangla
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Sample-এর output.

amar
sonar
bangla
done
Run in Compiler
Exercise 2মাঝারি

Nadia-র রান্নাঘরে একসাথে কয়েকটা পদ রান্না হয়। প্রতিটা পদের একটা রান্নার সময় আছে। সবগুলো একই মুহূর্তে শুরু করো, প্রতিটা আলাদা promise হিসেবে, আর যার promise ভরবে তার নাম print করো।

Input. n, তারপর n টা লাইন, প্রতিটায় একটা পদের নাম আর millisecond-এ রান্নার সময়।

Output. পদগুলোর নাম, যে ক্রমে রান্না শেষ হয়।

Constraints. 1 <= n <= 8। সময়গুলো আলাদা আলাদা, 10-এর গুণিতক, 10 থেকে 150।

Sample. নিচের input।

3
dal 40
bhaji 10
roti 20
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Sample-এর output.

bhaji
roti
dal
Run in Compiler
Exercise 3কঠিন

Maria দেখতে চায় একটা chain নিজের ভাগ্য নিজে কীভাবে ঠিক করে। ও একটা সংখ্যা দিয়ে শুরু করে, আর command থেকে একটা promise chain বানায়। add k একটা then-এ k যোগ করে। fail একটা then-এ error throw করে। fix k হলো একটা catch, যেটা value k দিয়ে সামলে নেয়। Chain শেষে কী দাঁড়াল, print করো।

Input. শুরুর সংখ্যা, তারপর n, তারপর n টা command, প্রতিটা নিজের লাইনে।

Output. এক লাইন: শেষ value-সহ fulfilled v, অথবা chain rejected অবস্থায় শেষ হলে rejected।

Constraints. 1 <= n <= 20। প্রতিটা সংখ্যা 0 থেকে 1000।

Sample. নিচের input।

5
4
add 3
fail
add 100
fix 1
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Sample-এর output.

fulfilled 1
Run in Compiler

সচরাচর যে প্রশ্নগুলো আসে

  • Promise আর callback কি একই জিনিস?

    না, যদিও promise ভেতরে callback ব্যবহার করে। সাধারণ callback-এ তুমি নিজের function ধীর code-এর হাতে দাও, আর ভরসা রাখো সে একবারই ডাকবে। Promise-এ ধীর code তোমার হাতে একটা token দেয়, আর তুমি তার গায়ে নিজের function লাগাও। তোমার function যেন বড়জোর একবার চলে, সেটা promise নিজেই দেখে।

  • await কি পুরো program আটকে দেয়?

    না। ও থামায় শুধু যে async function-এর ভেতরে বসে আছে, সেটাকে। Caller আর file-এর বাকিটা চলতে থাকে, loop-ও পালা নিতে থাকে। এই কারণেই ওপরে "countdown started" আগে আগে print হয়েছে।

  • then বা await ছাড়া promise থেকে value বের করা যায়?

    না। পড়ার মতো কোনো p.value নেই। Value তোমার code-এ পৌঁছায় শুধু then, catch, finally বা await দিয়ে। এটা ইচ্ছে করেই এমন: value তৈরি হওয়ার আগেই পড়ে ফেলা থেকে তোমাকে আটকায়।

  • Node promise-কে Promise { ... } হিসেবে print করে কেন?

    এটা debugging-এর জন্য object-টার Node-এর নিজের আঁকা ছবি। এতে অবস্থা দেখা যায়, আর settle হলে value-ও। তোমার code ওই ছবি কাজে লাগাতে পারে না। ওটা আছে শুধু দেখতে সাহায্য করার জন্য।

মূল কথা

  • ধীর function নিজের উত্তর return করতে পারে না, কারণ উত্তর আসে return-এর পরে; তাই সে একটা promise ফেরত দেয়।
  • Promise প্রথমে pending, তারপর value নিয়ে fulfilled বা কারণ নিয়ে rejected, আর settle হয় একবারই।
  • then value এলে একটা function চালায় আর নতুন promise ফেরত দেয়, তাই ধাপগুলো সমান লাইনে জোড়া যায়।
  • catch chain বেয়ে নামা rejection সামলায়, আর তার পরে chain আবার এগোয়।
  • async function-এর ভেতরে await promise settle না হওয়া পর্যন্ত শুধু ওই function-টাকে থামায়।
  • আরও গভীরে যেতে চাইলে: ভেতরের কলকবজা, microtask, একটা await-এর কটা পালা লাগে, thenable আর unhandled rejection (Pro)।

এরপর: promise-এর প্রতিটা method, একটা একটা করে, আর প্রতিটা then কী ফেরত দেয় তার নিয়ম।

lesson ১ শেষ

শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।

পরেরটা: Promise-এর প্রতিটা method, একটা একটা করে, আর প্রতিটা কী ফেরত দেয়