Module ১০ · error সামলানো: throw, try, catch, finally, আর stack trace পড়া
Error-এর ছবি: throw, catch, আর call stack-এর খুলে যাওয়া
এই lesson-এ যা শিখবে
- একটা function থেকে Error throw করো, আর
tryওcatchদিয়ে সেটা ধরো, যাতে একটা ভুল input-এ পুরো program থেমে না যায়। - মনে মনে দেখো, একটা throw কীভাবে call stack বেয়ে frame-এর পর frame নামে, যতক্ষণ না কোনো
catchওকে ধরে। - একটা Error-এর name, message আর stack পড়ো, আর বুঝিয়ে বলো কেন string না, Error throw করতে হয়।
Sadia-র একটা bakery আছে, বড় বড় order নেয়। প্রতি সন্ধ্যায় ওর staff পরের দিনের quantity একটা sheet-এ টাইপ করে, আর ছোট একটা program সব যোগ করে oven-এর হিসাব দেয়। গত মঙ্গলবার কেউ 10-এর জায়গায় 1o লিখে ফেলেছিল, শূন্যের বদলে ইংরেজি o। Program কোনো আপত্তি করেনি। Total দেখিয়েছে NaN, oven-এর হিসাবে কাজের কিছুই ছিল না, আর সকালের আগে কেউ টেরই পায়নি।
Sadia-র ভাগ্নে তাড়াতাড়ি একটা সমাধান করে দিল: এখন program প্রথম ভুল entry পেলেই থেমে যায়। এটা আরও খারাপ। একটা typo এখন বাকি সব entry ঢেকে দেয়, ঠিকগুলোও। Sadia আসলে চায় মাঝামাঝি কিছু। ভুল entry-র নাম জোরে বলা হোক, আর sheet-এর বাকিটা তবুও গোনা হোক। Error ঠিক এই কাজের জন্যই, আর এই module সেটাই গুছিয়ে শেখাবে।
Value ভুল হলেও JavaScript প্রায়ই চলতেই থাকে
JavaScript-এর অনেক কাজ ভুল value পেলে থামে না। একটা অদ্ভুত value দিয়ে দেয়, তারপর এগিয়ে যায়। Number("1o") হলো NaN, আর NaN-এর সাথে যা-ই যোগ করো, NaN। Module 2-এ এটা দেখেছ।
const sheet = ["12", "1o", "7"];
let total = 0;
for (const entry of sheet) total += Number(entry);
console.log(total);
console.log(Number("1o"));
NaN
NaN
Program শেষ পর্যন্ত চলেছে আর print করেছে NaN। কোন entry-তে সমস্যা, কিছুই বলেনি। চুপচাপ ভুল উত্তর সবচেয়ে দামি bug, কারণ সে ছড়ায়। এখানে ছড়াতে ছড়াতে oven পর্যন্ত পৌঁছে গেছে।
throw function-টা থামিয়ে দেয়
ওষুধ হলো, যে জায়গাটা ভুলটা বোঝে, সেখানেই ভুল value-টা ফিরিয়ে দেওয়া। throw statement ঠিক এটাই করে। সে চলতি function-টা সাথে সাথে থামিয়ে দেয়, আর সমস্যাটা ওপরের দিকে পাঠায়। Throw করো একটা Error object, যেটা বানাও new Error(message) দিয়ে, যেখানে message বলে কী গোলমাল হয়েছে।
function readQty(text) {
if (!/^\d+$/.test(text)) throw new Error("not a number: " + text);
return Number(text);
}
console.log(readQty("12"));
console.log(readQty("1o"));
console.log(readQty("7"));
12
Error: not a number: 1o
প্রথম call 12 return করে। দ্বিতীয়টা throw করে, আর কেউ ধরছে না বলে Node পুরো program থামিয়ে দেয়, বলে Error: not a number: 1o। তৃতীয় লাইন কখনো চলেই না। এটাই ভাগ্নের সমাধান: জোরে বলে ঠিকই, কিন্তু সবকিছু শেষ করে দেয়।
/^\d+$/ pattern-টার মানে "শুরু থেকে শেষ পর্যন্ত শুধু digit"। Module 16-এ এটা আবার আসবে। আপাতত এটাকে পড়ো "পুরোটা কি digit?" হিসেবে।
try আর catch সমস্যাটা হাতে নেয়
try block বলে, "এটা চালাও, আর কিছু throw হলে program থামিয়ো না"। ঠিক পরের catch block বলে, তখন বদলে কী করতে হবে। Throw হওয়া Error-টা catch-এ আসে একটা parameter হয়ে, সাধারণত নাম দেওয়া হয় err।
function readQty(text) {
if (!/^\d+$/.test(text)) throw new Error("not a number: " + text);
return Number(text);
}
const sheet = ["12", "1o", "7"];
let total = 0;
for (const entry of sheet) {
try {
total += readQty(entry);
} catch (err) {
console.log("skipped " + entry + " (" + err.message + ")");
}
}
console.log("total " + total);
skipped 1o (not a number: 1o)
total 19
Sadia এটাই চেয়েছিল। ভুল entry-র নাম বলা হলো, কারণসহ। ঠিক entry-গুলো তবুও গোনা হলো। try কোথায় বসেছে, খেয়াল করো: loop-এর ভেতরে। একটা throw শুধু loop-এর ওই একটা পালা শেষ করে, পরের entry নতুন করে শুরু হয়। একই try পুরো loop-এর চারপাশে বসালে প্রথম ভুল entry-তেই loop শেষ হয়ে যেত।
কিছু throw না হলে catch পুরোটাই বাদ যায়। কিছু throw হলে try block-এর বাকিটা বাদ যায়। উদাহরণে 1o-এর জন্য total += readQty(entry) কিছুই যোগ করে না, কারণ readQty কখনো return-ই করেনি।
Throw call stack বেয়ে নামে
আসল program-এ throw খুব কমই catch-এর ঠিক পাশে থাকে। একটা function আরেকটাকে call করে, সেটা আরেকটাকে, আর সবচেয়ে ভেতরেরটা সমস্যা খুঁজে পায়। Module 5 এটাকে এঁকেছিল call stack হিসেবে: প্রতিটা call ওপরে একটা frame বসায়, প্রতিটা return একটা সরায়।
Throw-ও এই stack-টাই ব্যবহার করে, ওপর থেকে নিচে। readQty throw করলে ওর নিজের frame শেষ হয়। ওর caller যদি call-টা try দিয়ে না মোড়ে, caller-এর frame-ও শেষ, এভাবে নিচের দিকে। এটাকে বলে unwinding, মানে stack খুলে যাওয়া। এটা থামে প্রথম সেই frame-এ, যে call-টা করেছিল একটা try-এর ভেতরে। ওই frame-এর catch চলে, আর program ওখান থেকে এগোয়।
function readQty(text) {
if (!/^\d+$/.test(text)) throw new Error("not a number: " + text);
return Number(text);
}
function addLine(text) {
const qty = readQty(text);
console.log("added " + qty);
return qty;
}
function importSheet(sheet) {
let sum = 0;
for (const text of sheet) sum += addLine(text);
return sum;
}
try {
console.log(importSheet(["12", "1o", "7"]));
} catch (err) {
console.log("import failed: " + err.message);
}
added 12
import failed: not a number: 1o
added 12 print হয়, তারপর দ্বিতীয় লাইন readQty-এর ভেতরে throw করে। addLine আর print করে না, importSheet কোনো যোগফল return করে না, আর চলে top-level-এর catch। এখানে একটা throw পুরো দুটো function-এর কাজ বাতিল করে দিল। এটা ঠিক কি না, program-এর ওপর নির্ভর করে। Sadia-র sheet-এর জন্য ঠিক না, ওখানে try থাকা উচিত loop-এর ভেতরে। কিন্তু এমন config file, যেটা হয় পুরো ঠিক, নয়তো একদমই ব্যবহার হবে না, তার জন্য এটাই ঠিক।
নিচের widget ওই program-এর throw-টা আবার চালিয়ে দেখায়, একটা আসল run থেকে বানানো। প্রতিটা function শুরু হলে ওর frame বসে। তারপর throw ওগুলোকে একটা একটা করে সরায়। Widget খুলে যাওয়াটা return-এর মতো করে দেখায়, কিন্তু এখানে কোনো frame কোনো value return করে না। Error পার হয়ে যাওয়ার সময় প্রতিটা শুধু শেষ হয়ে যায়।
দুবার ধাপে ধাপে দেখো। প্রথমবার দেখো stack চারটা frame পর্যন্ত বাড়ছে। দ্বিতীয়বার দেখো খুলে যাওয়াটা: readQty-এর নিচের কেউ কোনো value পায় না, আর একদম নিচের frame-টাই সেই জায়গা যেখান থেকে program এগোয়।
একটা Error-এর সাথে থাকে name, message আর stack
Error একটা object, আর ওর তিনটা property রোজ কাজে লাগে। name বলে কোন ধরনের error, যেমন Error বা TypeError। message হলো তুমি যে বাক্যটা দিয়েছিলে। stack একটা text, যেটা Node ভরে দেয় Error তৈরির সময়: name আর message, তারপর প্রতিটা frame-এর জন্য এক লাইন, ওপরের frame আগে।
JavaScript নিজেও Error throw করে। undefined-এর কোনো property পড়তে গেলে throw হয় একটা TypeError, Module 8 যেমন দেখিয়েছিল। ওগুলো ধরো একদম নিজেরগুলোর মতোই।
function firstLetter(user) {
return user.name[0];
}
try {
firstLetter({});
} catch (err) {
console.log(err.name);
console.log(err.message);
console.log(err instanceof TypeError, err instanceof Error);
}
TypeError
Cannot read properties of undefined (reading '0')
true true
{}-এর কোনো name নেই, তাই user.name হলো undefined, আর ওর [0] পড়তে গেলে throw হয়। তুমি কোথাও throw লেখোনি, তবু catch একটা পুরো Error পেয়েছে। instanceof বলে দেয় ওটা কোন ধরনের: TypeError একই সাথে একটা Error-ও। Lesson 02 প্রতিটা built-in ধরন আর প্রতিটার জন্য Node-এর আসল message-এর তালিকা দেবে।
String না, Error throw করো
JavaScript যেকোনো value throw করতে দেয়: throw "too big" চলে। এটা কোরো না। String-এর কোনো name, message বা stack নেই, তাই catch বুঝতে পারে না কী হয়েছে বা কোথায়। যে code err.message পড়ে, সে পায় undefined। নিচের প্রথম ভুলটা এটাই দেখায়। সবসময় throw করো new Error(...), বা ওর কোনো ধরন।
এক নজরে error
throw new Error("what went wrong"); // stop this function, hand the Error upward
try { // run this block...
risky(); // ...if it throws, jump to catch
} catch (err) { // err is the thrown Error
err.name; err.message; err.stack; // its kind, its sentence, its frames
}
// the rest of the program goes on here
Input হলো item-এর একটা তালিকা, প্রতিটা name:price। কোনো item ভাঙা হলে parseItem function পরিষ্কার একটা message দিয়ে throw করে। Loop প্রতিটা item আলাদা করে ধরে, ঠিকগুলো print করে, বাকিগুলো গোনে।
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
function parseItem(text) {
const parts = text.split(":");
if (parts.length !== 2) throw new Error("expected name:price in " + text);
const price = Number(parts[1]);
if (!Number.isInteger(price) || price <= 0) throw new Error("bad price in " + text);
return { name: parts[0], price };
}
let refused = 0;
for (const text of tokens) {
try {
const item = parseItem(text);
console.log(item.name.padEnd(8) + String(item.price).padStart(5));
} catch (err) {
refused++;
console.log("refused: " + err.message);
}
}
console.log(refused + " refused");
tea 20
refused: bad price in cake:x2
bun 15
refused: expected name:price in samosa
refused: bad price in naan:-5
lassi 60
3 refused
প্রতিটা message item-টার নাম বলে, তাই একজন মানুষ কয়েক সেকেন্ডে ঠিক করতে পারে। try একটা item ঢাকে, তাই cake:x2 আর samosa থামাতে পারে না bun বা lassi-কে। প্রথম program-এর চুপচাপ NaN-এর সাথে মিলিয়ে দেখো।
এই program JavaScript-কে দিয়ে তিনটা call গভীরে একটা TypeError throw করায়, তারপর stack-এর frame-গুলো print করে। ছোট helper-টা শুধু main.js-এর লাইনগুলো রাখে আর folder-টা কেটে দেয়, তাই output সব machine-এ একই।
function cityOf(order) {
return order.customer.city.toUpperCase();
}
function label(order) {
return order.id + " to " + cityOf(order);
}
function printLabels(orders) {
for (const o of orders) console.log(label(o));
}
try {
printLabels([{ id: 7, customer: { city: "Khulna" } }, { id: 8, customer: {} }]);
} catch (err) {
console.log(err.name + ": " + err.message);
const frames = err.stack.split("\n").filter((l) => l.includes("main.js"));
for (const f of frames) console.log(f.trim().replace(/\(.*main\.js/, "(main.js"));
}
7 to KHULNA
TypeError: Cannot read properties of undefined (reading 'toUpperCase')
at cityOf (main.js:2:30)
at label (main.js:5:30)
at printLabels (main.js:8:39)
at Object.<anonymous> (main.js:11:3)
প্রথম label print হয়। Order 8-এর কোনো city নেই, তাই city হলো undefined, আর ওটার ওপর toUpperCase call করতে গেলে throw হয়। Frame-গুলো ওপর থেকে পড়ো: cityOf ভেঙেছে, ওকে call করেছে label, ওটাকে printLabels, আর printLabels-কে top-level code। প্রতিটা frame শেষ হয় main.js, একটা লাইন নম্বর আর একটা column দিয়ে। Lesson 02 এমন একটা লাইন লাইন ধরে পড়বে।
এটা কোথায় কাজে লাগে
- Node.js: file না থাকলে
fs.readFileSyncENOENT-এর মতোcode-ওয়ালা একটা Error throw করে, আর program-গুলো সেটা ধরে একটা ভদ্র message দেখায়। - Express: route handler-এ throw হওয়া error চলে যায় চারটা parameter-ওয়ালা একটা error-handling middleware-এ, যেটা server crash না করিয়ে 500 response পাঠায়।
- Chrome DevTools: Console প্রতিটা না-ধরা error stack-সহ দেখায়, আর প্রতিটা frame source-এর ওই লাইনের একটা link।
- Sentry: app থেকে crash report জোগাড় করে, আর error-এর name আর stack frame ধরে ওগুলোকে দলে ভাগ করে।
যে ভুলগুলো সবাই করে
1. catch ছাড়া একটা try
try {
console.log(Number("12"));
}
console.log("done");
Node file-টা নিতেই রাজি হয় না, বলে SyntaxError: Missing catch or finally after try। try-এর পরে লাগবে একটা catch, একটা finally, বা দুটোই। finally দেখাবে Lesson 02।
2. String throw করা
function checkQty(qty) {
if (qty > 500) throw "too big";
return qty;
}
try {
checkQty(900);
} catch (err) {
console.log("problem: " + err.message);
console.log(typeof err);
}
problem: undefined
string
catch print করে problem: undefined, কারণ string-এর কোনো message নেই। কোনো stack-ও নেই, তাই কেউ বলতে পারবে না কোথা থেকে এল। লেখো throw new Error("too big")।
3. try-এর ভেতরে বানানো নাম, বাইরে ব্যবহার
try {
const total = Number("12") + Number("7");
} catch (err) {
console.log(err.message);
}
console.log(total);
ReferenceError: total is not defined
try একটা block, তাই ওর ভেতরে বানানো const বা let বাঁচে শুধু ওর ভেতরেই, Module 6 যেমন শিখিয়েছিল। Node থেমে যায়, বলে ReferenceError: total is not defined। try-এর আগে let total = 0; লেখো, তারপর ভেতরে assign করো।
Sadia-র একটা bakery আছে, বড় order নেয়। ওর staff প্রতিটা quantity একটা sheet-এ টাইপ করে, আর কেউ কেউ ভুল টাইপ করে। একটা ভুল entry-র জন্য পুরো sheet আটকে যাওয়া চলবে না। প্রতিটা entry আলাদা রায় পাবে, আর ঠিকগুলো যোগ হবে।
Input. প্রথম লাইনে n। পরের n টা token হলো entry-গুলো, প্রতি লাইনে একটা।
Output. প্রতিটা entry-র জন্য এক লাইন, ক্রম মেনে। ঠিক entry হলে ok আর তার value, শুরুর শূন্য ছাড়া। Digit ছাড়া অন্য কিছু থাকলে bad, entry-টা, একটা colon আর not a number। শুধু digit, কিন্তু 1 থেকে 500-এর বাইরে হলে bad, entry-টা, একটা colon আর out of range। শেষ লাইনে total, ঠিক value-গুলোর যোগফল, from, ওদের সংখ্যা আর good।
Constraints. 1 <= n <= 1000; Every entry is 1 to 8 characters with no spaces..
Sample.
6
12
007
1e2
0
ten
500
ok 12
ok 7
bad 1e2: not a number
bad 0: out of range
bad ten: not a number
ok 500
total 519 from 3 good
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"));
Run in Compiler
bad-quantities নামে grade হয়।
Imran crash পড়া শিখছে। ওর teacher board-এ call-এর একটা শিকল আঁকেন: main প্রথম function-কে call করে, সেটা পরেরটাকে, এভাবে চলতে থাকে। কিছু function নিজের call-টা try আর catch দিয়ে মুড়ে রাখে, কিছু রাখে না। শেষ function-টা throw করে। Imran-কে বলতে হবে error-টা কে ধরে, আর পথে কয়টা function মাঝপথে থেমে গেল।
Input. প্রথম লাইনে t, শিকলের সংখ্যা। প্রতিটা শিকল শুরু হয় n লেখা একটা লাইন দিয়ে। তারপর n টা লাইন, সবচেয়ে বাইরের function আগে, প্রতিটায় একটা নাম আর catch বা pass। catch লেখা function নিজের একমাত্র call-টা try আর catch দিয়ে মুড়ে রাখে। শেষ function সবসময় pass, আর সে-ই throw করে।
Output. প্রতিটা শিকলের জন্য এক লাইন। কোনো function error ধরলে তার নাম, তারপর catches,, মাঝপথে থেমে যাওয়া function-এর সংখ্যা আর frames unwound। কেউ না ধরলে uncaught,, তারপর n আর frames unwound।
Constraints. 1 <= t <= 100; 1 <= n <= 1000, and the n of all chains add up to at most 20000.; A name is 1 to 10 letters..
Sample.
3
4
main catch
load catch
parse pass
readQty pass
2
main pass
boom pass
3
app catch
api pass
db pass
load catches, 2 frames unwound
uncaught, 2 frames unwound
app catches, 2 frames unwound
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"));
Run in Compiler
who-catches নামে grade হয়।
Imran ক্লাসের জন্য ছোট একটা calculator লেখে। প্রতিটা লাইন a op b। ভাঙা লাইন নিজের error print করবে, আর বাকিদের চলতে দেবে। তিনটা জিনিস ভুল হতে পারে, আর প্রতিটার নিজের ধরনের Error। Operand integer না হলে throw হয় একটা TypeError, message not a number: আর operand-টা। + - * / ছাড়া অন্য operator হলে throw হয় একটা Error, message unknown operator আর operator-টা। শূন্য দিয়ে ভাগ করলে throw হয় একটা RangeError, message division by zero।
Input. n, তারপর n টা লাইন, প্রতিটায় a op b, তিনটা token।
Output. প্রতিটা হিসাবের জন্য এক লাইন। ফল print করো, /-এর বেলায় Math.trunc দিয়ে শূন্যের দিকে কাটা। ভাঙা লাইনে print করো error, Error-এর name, একটা colon আর ওর message।
Constraints. 1 <= n <= 1000। ঠিক operand হলো -1000 থেকে 1000-এর integer।
Sample. নিচের input।
5
7 + 5
9 / 0
3 x 4
ten * 2
-9 / 2
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.
12
error RangeError: division by zero
error Error: unknown operator x
error TypeError: not a number: ten
-4
Run in Compiler
সচরাচর যে প্রশ্নগুলো আসে
Throw আর crash কি একই জিনিস?
না। Throw একটা অনুরোধ: "ওপরে কেউ এটা সামলাও"। Crash হয় তখনই, যখন stack-এর একদম নিচ পর্যন্ত কোনো
catchওকে ধরে না। তখন Node Error আর ওর stack print করে, আর program থামিয়ে দেয়।নিরাপদ থাকতে সবকিছুর চারপাশে try দিয়ে দিই?
না। যে
catchকাজের কিছু করতে পারে না, সে শুধু সমস্যাটা লুকায়, মানে চুপচাপ NaN নতুন চেহারায় ফিরে আসে।tryবসাও সেখানে, যেখানে জানো বদলে কী করবে: লাইনটা বাদ দেবে, আবার জিজ্ঞেস করবে, বা একটা message দেখাবে। Lesson 04 এটাকে একটা নিয়ম বানাবে।catch কি আমার নিজের typo-ও ধরে?
হ্যাঁ, আর এটা একটা ফাঁদ। ভুল বানানের একটা নাম অন্য যেকোনো Error-এর মতোই
ReferenceErrorthrow করে, আর ঢালাও একটাcatchওটাকে "ভুল input" বলে জানাবে। Lesson 06-এর bug gallery এটা দেখাবে, সমাধানসহ: সামলানোর আগে error-এর ধরন পরখ করো।NaN যদি error না হয়, তাহলে ওটা কী?
NaNএকটা number value, যার মানে "ঠিকঠাক সংখ্যা না"। কাজগুলো throw না করে এটা দিয়ে দেয়। এই কারণেই input নিজে পরখ করো, আর ভুল হলে throw করো।
মূল কথা
throw new Error(message)চলতি function থামায় আর Error-টা ওপরে পাঠায়।try { } catch (err) { }throw হওয়া Error ধরে, আর programcatch-এর পর থেকে এগোয়।- Throw call stack খুলে ফেলে, frame-এর পর frame, যতক্ষণ না এমন একটা frame আসে যে call করেছিল
try-এর ভেতরে। tryকোথায় বসাও, সেটাই ঠিক করে একটা error কতটা কাজ বাতিল করবে।- Error-এর আছে
name,messageআরstack; throw করো Error, কখনো string না। - আরও গভীরে যেতে চাইলে: Error-এর ভেতরে, completion record, try-এর খরচ, আর একটা Error বানাতে আসলে কত লাগে (Pro)।
এরপর: error সামলানোর প্রতিটা অংশ, একটা একটা করে, finally, built-in ধরনগুলো আর তোমার নিজেরগুলো সহ।
lesson ১ শেষ
শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।
পরেরটা: Error সামলানোর প্রতিটা অংশ, একটা একটা করে: finally, built-in ধরন, নিজের error আর stack trace