Replace Exception with Test: ভেতরে ঢোকার আগে বোর্ড দেখো
Replace Exception with Test (Replace Exception with Precheck) refactoring শেখো — চায়ের দোকানের গল্প দিয়ে, before/after TypeScript আর C# code দিয়ে, TryParse-style pattern দিয়ে, check-then-act race condition trap দিয়ে, আর modern তৃতীয় পথ হিসেবে Result type দিয়ে।
চায়ের দোকানে ঢোকার আগে বোর্ড দেখো
ধরো, তোমাদের college-এ দুইজন student — রুবেল আর সুমাইয়া। দুইজনই বিকেলে চা খেতে চায়।
রুবেলের একটা system আছে। যখনই চা খেতে ইচ্ছে করে, সে সোজা চায়ের দোকানে হেঁটে যায়, কাউন্টারে দাঁড়িয়ে চা চায়। দোকান বন্ধ থাকলে জামাল ভাই — কম ধৈর্যের আর বড় গলার দোকানদার — চিৎকার দেন: "বন্ধ! দেখতে পাচ্ছো না?!" — রুবেল ফিরে আসে। এটা দিনে চার-পাঁচবার হয়। প্রতিবার একই নাটক: লম্বা হাঁটা, চিৎকার, লজ্জায় ফিরে আসা। জামাল ভাই ক্লান্ত। রুবেল সোজামুখে বলে এটা নাকি তার "দোকান খোলা আছে কিনা check করার পদ্ধতি।"
সুমাইয়ার আলাদা system আছে। দোকানের গেটে একটা ছোট বোর্ড ঝোলানো: খোলা নাকি বন্ধ — শাটার উঠলে-নামলে বোর্ড বদলায়। সুমাইয়া corridor থেকে হাঁটতে হাঁটতে বোর্ডটা এক ঝলক দেখে নেয়। বোর্ড খোলা বলছে — সে ঢোকে। বোর্ড বন্ধ বলছে — সে সরাসরি library যায়, এক পাও নষ্ট হয় না। দুই সেকেন্ড, কোনো নাটক নেই, কোনো চিৎকার নেই। আর জামাল ভাই তার গলা বাঁচিয়ে রাখেন সত্যিকারের জরুরি মুহূর্তের জন্য — যেদিন রান্নাঘরে সত্যিই আগুন লেগেছিল, সেদিন তার চিৎকারে এক মিনিটেরও কম সময়ে সবাই বেরিয়ে গিয়েছিল। কারণ সবাই জানত, ছোট কারণে তিনি চিৎকার দেন না।
শেষের কথাটাই এই পুরো post-এর মূল কথা। বিকেলে দোকান বন্ধ থাকা কোনো জরুরি অবস্থা না। এটা স্বাভাবিক, predictable, প্রতিদিনের ঘটনা। দোকানদারের চিৎকার দিয়ে এটা জানা মানে একটা emergency system-কে routine কাজে ব্যবহার করা। আর এটা ধীরে ধীরে সবাইকে শেখায় emergency system-কে ignore করতে।
Code-এ রুবেলের পদ্ধতি হলো try/catch — expected situation-এর জন্য exception throw করা। সুমাইয়ার বোর্ড হলো সস্তা একটা upfront check — if (canteen.isOpen())। আজকের refactoring, Replace Exception with Test, রুবেলের নাটককে সুমাইয়ার এক ঝলকে পরিণত করে।
Replace Exception with Test কী?
Exception একটা কাজের জন্য দারুণ: সত্যিকারের অপ্রত্যাশিত সমস্যা announce করতে, যেগুলো ignore করা চলবে না। Replace Error Code with Exception post-এ আমরা ঠিক সেজন্যই exceptions-এর প্রশংসা করেছিলাম। কিন্তু যেকোনো powerful tool-এর misuse হয়। Exception-এর classic misuse হলো: ordinary, predictable, সহজে আগেভাগে check করা যায় এমন situation-এ try/catch ব্যবহার করা।
Map-এ missing key। Empty list। যে string number হতেও পারে, নাও হতে পারে। Config value যেটা set নাও থাকতে পারে। এগুলো কোনো emergency না। এগুলো মঙ্গলবারের মতো স্বাভাবিক। এই মঙ্গলবারগুলো try/catch দিয়ে handle করা code-এর তিনটা সমস্যা আছে।
১. Reader-কে মিথ্যা বলে। একটা try/catch block বলে "এখানে কিছু অস্বাভাবিক হতে পারে।" কিন্তু যখন সেই "অস্বাভাবিক" জিনিসটা হলো অর্ধেক call-এ ঘটা একটা missing default, reader বুঝতেই পারে না কোনটা normal আর কোনটা rare। Control flow catch-এর ভেতরে লুকিয়ে থাকে, দেখা যায় না।
২. ধীর। Exception throw করলে stack trace build হয় আর call stack unwind হয় — এটা simple comparison-এর চেয়ে শত থেকে হাজার গুণ বেশি কাজ। Microsoft engineers "exceptions for control flow" কে classic performance sin-এর তালিকায় রাখে। বাস্তব report আছে যেখানে throw-per-row কে precheck দিয়ে replace করায় "চিরকাল লাগছিল" এমন import হঠাৎ দ্রুত হয়ে যায়।
৩. সত্যিকারের alarm-কে সস্তা করে। সারাদিন routine কারণে exceptions fire হলে কেউ সেগুলোকে seriously নেয় না। Log-এ শুধু noise জমে। সত্যিকারের corruption signal করা exception হাজার fake exception-এর ভিড়ে হারিয়ে যায়। যে watchman ঘণ্টায় ঘণ্টায় চিৎকার দেয়, আগুনের সময় তাকে কেউ শোনে না।
Replace Exception with Test বলে: catch block যে condition-টা চুপচাপ সামলাচ্ছে সেটা খুঁজে বের করো, আর কাজ করার আগেই সেটার জন্য explicitly test করো। Martin Fowler-এর দ্বিতীয় edition-এ এর নাম Replace Exception with Precheck — নামটাই বলে দেয় কী করতে হবে।
এক লাইনে summary: যদি সস্তায় জিজ্ঞেস করতে পারো "দোকান কি খোলা?" তাহলে জিজ্ঞেস করো — আর watchman-এর চিৎকার (exception) শুধু সেই জিনিসগুলোর জন্য রাখো যেগুলো কেউ আগে থেকে check করতে পারত না।
এই দুটো twin refactoring একে অপরের শত্রু না; এগুলো একটা নিয়মের দুটো হাত। Expected condition → আগে check করো। Unexpected problem → throw করো। দুটো জিনিস master করা মানে বুঝতে পারা তুমি কোন situation-এ আছো। আর modern দুনিয়া তৃতীয় এমনকি চতুর্থ পথও যোগ করেছে। তাই deeper যাওয়ার আগে পুরো toolbox-টা এক ছবিতে দেখো।
চারটা tool একটা decision table-এও fit হয়। এটা bookmark করে রাখো — error handling নিয়ে বেশিরভাগ code-review argument এটাই answer করে।
| Situation | সঠিক tool | চায়ের দোকানের ভাষায় |
|---|---|---|
| Expected condition, সস্তা safe check, নিজের state | Precheck (if) — এই refactoring | বোর্ড দেখো |
| Expected condition, shared state যেটা বদলে যেতে পারে | Atomic Try-style call, handle করো না পেলে | একবার knock করো; সাড়া না পেলে এগিয়ে যাও |
| Expected failure যেটা কারণ বহন করে | Result / Either type | বোর্ডে লেখা: বন্ধ, পরীক্ষা শেষে ৫টায় খুলবে |
| সত্যিই অস্বাভাবিক, কেউ আগে check করতে পারত না | Exception — twin refactoring | রান্নাঘরে আগুন |
কখন এটার দরকার হয়?
Real code-এ এই pattern গুলো দেখলে সতর্ক হও।
- Lookup-এর আশেপাশে try/catch। Map/dictionary access, array index, বা property read-এর failure catch করা — যেখানে
has(),length, বা null check আগেই answer করতে পারত। - Catch block যেটা শুধু default return করে।
catch { return DEFAULT_PRICE; }হলো classic sign। Default value হলো emergency-র একদম বিপরীত জিনিস। - Catch দিয়ে parse করা।
parseInt-style বাParse()-style call-কে try/catch দিয়ে wrap করা যেখানেTryParseবা validity check আছে। User input যেটা number নাও হতে পারে সেটা expected — এজন্যই users-এর keyboard আছে। - Loop-এ exceptions fire হওয়া। Profiler আর log-এ প্রতি মিনিটে হাজার হাজার একই exception দেখা যাচ্ছে। প্রতিটাই একটা মঙ্গলবারের জন্য watchman-এর চিৎকার।
- "Expected exception" comment। Code নিজেই যখন বলে
// this is normal, ignorecatch block-এর ভেতরে, author জানতেন সত্যিটা কিন্তু ভুল tool ব্যবহার করেছেন।
ধরো, canteen app-এর team এক সপ্তাহের production log audit করল আর প্রতিটা exception classify করল। ফলাফলটা এই post-এর সবচেয়ে convincing chart।
পঁচানব্বই শতাংশ চিৎকার ছিল মঙ্গলবারের জন্য। পাঁচ শতাংশ সত্যিকারের failure — একটা corrupted price file, একটা database timeout — হাজার হাজার routine entry-র নিচে চাপা পড়েছিল। আর যেদিন price file corrupt হয়েছিল, ছয় ঘণ্টা কেউ বুঝতেই পারেনি কারণ error log সবসময় ওরকমই দেখায়। Alert fatigue কোনো metaphor না; এটা একটা measurable production risk।
আর যেসব চিহ্ন দেখলে এই refactoring করা উচিত না — এগুলোও মনোযোগ দিয়ে পড়ো।
- Check আর act-এর মাঝে state বদলে যেতে পারে। তুমি check করলে file আছে; অন্য process delete করল; তোমার open call fail করল। Concurrent বা shared-resource situation-এ check-then-act-এ built-in race condition থাকে (classic TOCTOU — time-of-check to time-of-use সমস্যা)। সেখানে operation চেষ্টা করে failure catch করাটাই সঠিক, atomic approach।
- কোনো সস্তা test নেই। Operation succeed করবে কিনা জানতে হলে যদি basically operation-টাই করতে হয়, তাহলে precheck কাজ দ্বিগুণ করে। Exception রাখো অথবা Try-style API ব্যবহার করো।
- Condition সত্যিই অস্বাভাবিক। সত্যিকারের corruption signal করা exception-কে চুপচাপ default দিয়ে "fix" করিও না — এটা real bug লুকিয়ে ফেলে।
একটু ভাবো, চায়ের দোকানের গল্পেই race condition আছে। সুমাইয়ার বোর্ড কাজ করে কারণ বোর্ড আর শাটার একই সাথে একই মানুষ বদলায়। কিন্তু ধরো সুমাইয়া বোর্ডে "খোলা" দেখল, দুটো corridor হাঁটল, আর হাঁটতে হাঁটতে দোকান বন্ধ হয়ে গেল — check-এর সময় সত্যি ছিল, use-এর সময় মিথ্যা। College-এ দুই মিনিটের হাঁটার জন্য কে পরোয়া করে। কিন্তু দুটো thread আর shared file-এর জন্য একই gap একটা bug। নিচে canteen-কে state machine হিসেবে দেখো, দুই student-এর path সহ।
Before আর After এক ঝলকে
দেখো canteen app-এর price lookup, আগের version। Missing item code সম্পূর্ণ স্বাভাবিক — সপ্তাহে সপ্তাহে নতুন item যোগ হয় — কিন্তু code সেটাকে disaster মনে করছে।
// BEFORE: an expected case handled by emergency machinery
const DEFAULT_PRICE = 10;
function unitPrice(menu: Map<string, number>, itemCode: string): number {
try {
const price = menu.get(itemCode);
if (price === undefined) {
throw new Error(`No price for ${itemCode}`); // shout!
}
return price;
} catch {
return DEFAULT_PRICE; // ...and immediately apologize for shouting
}
}সৎভাবে পড়ো: function নিজেই exception throw করছে আর তিন লাইন পরে নিজেই catch করছে, শুধু একটা default deliver করতে। রুবেল ঢুকছে শুধু বের হওয়ার জন্য। Refactoring-এর পরে:
// AFTER: glance at the board first
const DEFAULT_PRICE = 10;
function unitPrice(menu: Map<string, number>, itemCode: string): number {
return menu.get(itemCode) ?? DEFAULT_PRICE;
}এক লাইন। Normal flow এক ঝলকেই বোঝা যাচ্ছে, কোনো hidden jump নেই, কোনো stack trace build হচ্ছে না, আর এই module-এর exception এখন আবার মানে রাখে। TypeScript-এ ?? operator explicit has() check-এর কাজ করছে; অন্য language-এ if (menu.has(itemCode)) লিখতে হত — একই idea, একই refactoring।
দুই student-এর approach পাশাপাশি, sequence হিসেবে — গুনে দেখো কার কতটা arrow লাগছে।
ধাপে ধাপে, নিরাপদভাবে
Mechanics সংক্ষিপ্ত, কিন্তু প্রতিটা ধাপে একটু ভাবতে হবে। Fowler-এর order follow করো।
ধাপ ১: Catch block ঠিক কোন condition সামলাচ্ছে সেটা চিহ্নিত করো। Catch block পড়ো আর এই sentence শেষ করো: "আমরা এখানে আসি যখন ___।" যদি উত্তর routine situation হয় ("item এখনো menu-তে নেই"), এগিয়ে যাও। উত্তরে যদি কিছু unpredictable থাকে ("database connection drop করেছে"), থামো — সেই অংশ exception-ই থাকবে।
ধাপ ২: সেই condition-এর জন্য সস্তা, side-effect-free test খোঁজো বা বানাও। map.has(key), array.length > 0, dict.ContainsKey, value !== undefined, Number.isFinite(n)। Test টা safe হতে হবে — কিছু না বদলিয়ে call করা যাবে — আর প্রতিটা call-এ run করার মতো সস্তা।
ধাপ ৩: সামনে test যোগ করো, catch রেখে দাও। এটা নিরাপদ intermediate state — belt আর braces দুটোই। New guard expected case handle করবে; পুরনো catch পেছনে থাকবে, সাময়িকভাবে।
// INTERMEDIATE: guard added, catch still present as a safety net
function unitPrice(menu: Map<string, number>, itemCode: string): number {
if (!menu.has(itemCode)) {
return DEFAULT_PRICE; // new: the board
}
try {
const price = menu.get(itemCode);
if (price === undefined) throw new Error(`No price for ${itemCode}`);
return price;
} catch {
return DEFAULT_PRICE; // old: should now be unreachable
}
}ধাপ ৪: Prove করো catch dead হয়ে গেছে, তারপর remove করো। Tests run করো। Production code-এ extra confidence চাইলে এক release catch-এর ভেতরে log করো — log-এ নীরবতাই তোমার proof। তারপর try/catch আর artificial throw delete করো।
ধাপ ৫: Race আর atomicity আবার check করো। শেষ একটা সৎ দেখা: test আর use-এর মাঝে কিছু বদলে যেতে পারে? একটা thread-এর owned in-memory map-এর জন্য — না, তুমি safe। File, network, database, shared cache-এর জন্য — সম্ভব হ্যাঁ, তখন catching-এ ফিরে যাও বা atomic Try-style API ব্যবহার করো।
ধাপ ৬: তিনটা case test করো। Present case, absent case, আর boundary (empty map, empty string, zero)।
Check-then-act race হলো একমাত্র জায়গা যেখানে এই refactoring আগে না থাকা bug introduce করতে পারে। if (file.exists()) file.open() try/catch-এর চেয়ে clean দেখাচ্ছে — কিন্তু shared disk-এ এটা ভুল, কারণ দুই লাইনের মাঝে file হারিয়ে যেতে পারে। "Seat available check করো, তারপর book করো" — যেকোনো multi-user system-এ একই trap। Rule of thumb: precheck সেই data-র জন্য perfect যেটা তুমি memory-তে exclusively own করো; shared, external, বা concurrent resource-এর জন্য single atomic operation prefer করো আর তার failure handle করো।
একটু deep কথা — throw করতে কত খরচ, আর Python কেন মাঝে মাঝে ভিন্নমত রাখে। Modern runtime zero-cost exception handling implement করে: try block-এ ঢুকতে কোনো খরচ নেই, কারণ handler location static table-এ থাকে। পুরো দাম দিতে হয় throw-এর সময় — stack trace capture, frame by frame unwind table walk, cleanup run, handler type match। একটা throw microseconds-এ; একটা comparison nanoseconds-এ; ratio সাধারণত দুই থেকে চার order of magnitude। এজন্যই "exceptions as control flow" প্রতিটা performance-sin list-এ থাকে। Python বন্ধুরা পাল্টা বলবে: Python culture EAFP prefer করে — "easier to ask forgiveness than permission" — মানে try/except KeyError সেখানে idiomatic যেখানে C# precheck করত (LBYL, "look before you leap")। দুটো fact দুই camp-এর মধ্যে শান্তি আনে। প্রথমত, Python-এর exception .NET বা JVM-এর মতো ব্যয়বহুল না, তাই performance argument সেখানে দুর্বল। দ্বিতীয়ত, EAFP আংশিকভাবে correctness argument: concurrent situation-এ try-and-ask-forgiveness atomic যেখানে check-then-act race করে — একই TOCTOU point। তাই deep rule হলো language-independent: cost measure করো, atomicity respect করো, আর expected case-কে expected-এর মতো দেখতে দাও। C# আর TypeScript-এ এটা প্রায় সবসময় মানে precheck বা Try-method; Python-এ এক লাইনের আশেপাশে tight except KeyError acceptable — কিন্তু broad except Exception দিয়ে default return করা প্রতিটা language-এই খারাপ।
একটা বড় real-life উদাহরণ
Canteen app-এর billing screen-এ একটা real performance story আছে। Students counter-এ item code টাইপ করে, আর month-end-এ app হাজার হাজার হাতে লেখা paper-register row import করে, অনেকগুলো malformed। প্রথম version সবকিছুতে exception ব্যবহার করত।
// BEFORE: three expected situations, three emergency responses
function billRow(menu: Map<string, number>, rawCode: string,
rawQty: string): number {
let qty: number;
try {
qty = strictParseInt(rawQty); // throws on "two", "", "3 cups"
} catch {
qty = 1; // expected: messy handwriting
}
let price: number;
try {
price = mustGet(menu, rawCode); // throws when code unknown
} catch {
price = DEFAULT_PRICE; // expected: new items
}
try {
return applyDiscount(price * qty); // throws when no discount set
} catch {
return price * qty; // expected: most days
}
}50,000 row import-এ যেখানে এক-তৃতীয়াংশ row messy, এই function হাজার হাজার exception throw করে — প্রতিটাই একটা মঙ্গলবারের জন্য stack trace build করছে। Import হামাগুড়ি দিয়ে চলত। Refactored version আগে জিজ্ঞেস করে।
// AFTER: glance at each board; exceptions retired from routine duty
function billRow(menu: Map<string, number>, rawCode: string,
rawQty: string): number {
// 1) Parse with a test, not a trap
const parsed = Number.parseInt(rawQty, 10);
const qty = Number.isNaN(parsed) ? 1 : parsed;
// 2) Look before you get
const price = menu.get(rawCode) ?? DEFAULT_PRICE;
// 3) Ask whether a discount exists before applying it
const discount = discountFor(rawCode); // returns rate or null
const total = price * qty;
return discount === null ? total : total * (1 - discount);
}প্রতিটা branch এখন দেখা যাচ্ছে। নতুন programmer উপর থেকে নিচে function পড়ে business-এর আসল shape দেখতে পাবে: messy quantity default করে one হয়, অজানা item default price পায়, discount optional। কিছু jump করছে না, কিছু unwind করছে না, আর import memory speed-এ চলছে। আর লক্ষ্য করো কী আমরা চুপ করাইনি: যদি discountFor-এর ভেতরে database connection fail করে, সেই exception এখনো fly করবে — কারণ সেটা আগুন, মঙ্গলবার না।
Team-টা same 50,000-row register file নিয়ে month-end import আগে আর পরে time করল।
ছিয়ানব্বই সেকেন্ড থেকে চার সেকেন্ড। Algorithm বদলায়নি, hardware বদলায়নি — শুধু সরানো হয়েছে হাজার হাজার stack trace যেগুলো routine প্রশ্নের উত্তর দিতে build হচ্ছিল। Business codebase-এ এটাই সবচেয়ে common "বিনামূল্যে" performance win, আর profiler তোমাকে খুঁজে দেবে: প্রতি মিনিটে হাজারে হাজারে exception count দেখলে সতর্ক হও।
C#-এ একই refactoring
.NET এই refactoring-কে বিশেষভাবে আনন্দদায়ক করে, কারণ standard library Try-style method দেয় — precheck আর action একটা atomic, race-free call-এ।
// BEFORE: exceptions doing routine work
public decimal UnitPrice(Dictionary<string, decimal> menu, string code)
{
try
{
return menu[code]; // KeyNotFoundException when absent
}
catch (KeyNotFoundException)
{
return DefaultPrice; // ...but absence is normal!
}
}
public int ParseQuantity(string raw)
{
try
{
return int.Parse(raw); // FormatException on "two cups"
}
catch (FormatException)
{
return 1;
}
}// AFTER: TryGetValue and TryParse — the check and the act in ONE step
public decimal UnitPrice(Dictionary<string, decimal> menu, string code)
{
return menu.TryGetValue(code, out var price) ? price : DefaultPrice;
}
public int ParseQuantity(string raw)
{
return int.TryParse(raw, out var qty) ? qty : 1;
}Try-pattern একটু বিশেষ attention deserve করে। TryGetValue আর TryParse আলাদা check-then-act pair-এর চেয়ে ভালো: এগুলো single lookup করে (দুটো না — ContainsKey আর তারপর indexer dictionary দুইবার search করে), আর এগুলো atomic, তাই race-condition চিন্তা উড়ে যায়। .NET এমনকি একটা analyzer ship করে, CA1854, যেটা ContainsKey + indexer pair দেখলে flag করে আর TryGetValue suggest করে।
C#-এ নিজের API design করার সময়, framework যেমন করে তেমন pair offer করো: throwing version callers-এর জন্য যারা absence-কে bug মনে করে (GetPrice), আর Try-version callers-এর জন্য যারা absence-কে মঙ্গলবার মনে করে (TryGetPrice)। তখন প্রতিটা caller তার নিজের expectation অনুযায়ী tool বেছে নেয়। এরকম API কেমন দেখতে:
দুটো arrow পড়ো: billing screen অজানা code-কে bug মনে করে (cashier system-এ নেই এমন কিছু scan করেছে — investigate করো!), তাই throwing version call করে। Import job অজানা code-কে routine handwriting mess মনে করে, তাই Try-version call করে। একই data, ভিন্ন expectation, ভিন্ন দরজা — আর দুই caller-ই ঠিক।
Balance-এর জন্য Python-এ একই lesson, যেখানে dictionary-র get method একটা default সহ precheck-and-act একটা call-এ করে — ঠিক TryGetValue-এর মতো:
DEFAULT_PRICE = 10
# Ravi style: emergency machinery for a routine question
def unit_price_ravi(menu: dict[str, int], code: str) -> int:
try:
return menu[code] # raises KeyError when absent
except KeyError:
return DEFAULT_PRICE
# Meena style: one atomic ask-with-default
def unit_price_meena(menu: dict[str, int], code: str) -> int:
return menu.get(code, DEFAULT_PRICE)
# Parsing with a test instead of a trap
def parse_qty(raw: str) -> int:
return int(raw) if raw.strip().isdigit() else 1EAFP-friendly Python-এও এখানে menu.get(code, default) universally preferred try/except-এর চেয়ে — কারণ যখন একটা one-line atomic tool আছে যেটা ঠিক যা বলতে চায় তাই বলে, সব culture একমত যে সেটাই জেতে।
IDE support
কোনো IDE এক-click "Replace Exception with Test" দেয় না — কোনো condition expected কিনা সেটা human judgment দরকার। কিন্তু surrounding tooling শক্তিশালী।
- .NET analyzers / Visual Studio / Rider: rule CA1854 ("Prefer Dictionary.TryGetValue") Quick Fix দিয়ে double-lookup pattern rewrite করে; IDE suggestion
int.Parse+ catch কেint.TryParse-এর দিকে নিয়ে যায়; ReSharper catch clause flag করে যেটা শুধু default return করে। - SonarLint / SonarQube (Java, C#, TypeScript): exceptions as control flow আর expected outcome গিলে ফেলা catch block-এর বিরুদ্ধে dedicated rule — cleanup-এর পরে team-wide watchdog হিসেবে useful।
- IntelliJ IDEA: "Catch block may ignore exception" আর "Exception used for control flow" inspection এই refactoring-এর candidate highlight করে।
- TypeScript + ESLint:
no-emptysilent catch block ধরে; compiler-এর strict null checking plus??আর optional chaining প্রায়ই পুরো try/catch-কে এক expression-এ গুলিয়ে দেয়। - Detector হিসেবে profiler: Visual Studio-এর Events view, dotnet-trace, আর Chrome DevTools সব exception count দেখায় — প্রতি মিনিটে হাজার হাজার identical exception spike মানে এই refactoring-এর সবচেয়ে জোরে আমন্ত্রণ।
সুবিধা আর ঝুঁকি
সৎ হিসাব — আর মনে রেখো, এই post আর Replace Error Code with Exception একটাই lesson, দুই ভাগে: expected condition-এর জন্য check, unexpected problem-এর জন্য exception। এই table হলো check দিকটা সঠিকভাবে apply করার বিষয়ে।
| সুবিধা | ঝুঁকি / খরচ |
|---|---|
| Normal flow visible হয়, উপর থেকে নিচে readable — catch block-এ hidden jump নেই | Check-then-act race (TOCTOU): shared resource-এ check-এর পরে state বদলে যেতে পারে — সেখানে atomic catch রাখো |
| Stack-trace building আর unwinding এড়ায় — hot path-এ অনেক গুণ দ্রুত | সস্তা test না থাকলে, precheck operation-এর কাজ দ্বিগুণ করতে পারে |
| Exception আবার মানে রাখে: throw আবার সত্যিই কিছু ভুলের signal দেয় | বেশি default করলে real bug লুকাতে পারে — চুপচাপ corruption ঢেকে রাখা throw করার চেয়ে খারাপ |
সাধারণত সংক্ষিপ্ত: ternary বা ?? পাঁচ লাইনের try/catch ceremony replace করে | অনেক caller-এ duplicate guard নিজেই smell হয়ে যায় — centralize করো |
| Log আর monitoring routine "expected exception"-এ ডুবে যাওয়া বন্ধ করে | Judgment দরকার: real anomaly-কে "expected" classify করলে আগুনের alarm লুকিয়ে যায় |
দ্রুত আর consistently এই judgment কীভাবে করবে? দুটো প্রশ্ন: condition কি expected? আর state কি তুমি exclusively own করো, নাকি shared আর changeable? যেকোনো situation এই দুটো axis-এ plot করো আর সঠিক tool সরাসরি পড়া যায়।
Bottom-left হলো এই refactoring-এর home ground: নিজের owned state-এ expected condition — precheck করো মনের আনন্দে। Bottom-right হলো Try-method zone: expected, কিন্তু shared — একটা atomic call, race করার কোনো gap নেই। উপরের অর্ধেক twin post-এর: সত্যিই unexpected জিনিস throw করবে — state তোমার হোক (broken invariant — ঠিক করার bug) বা shared হোক (corruption — boundary-তে catch করো, মানুষকে alert করো)।
Result আর Either type সম্পর্কে একটু কথা বলা দরকার — তৃতীয় পথটা সততার সাথে। Result type — Rust-এর Result, fp-ts-এর Either, plain TypeScript-এ typed union, C#-এ OperationResult-style library — ঠিক এই post-এর middle ground-এ shine করে: expected failure যেগুলো তবুও return করার মতো information বহন করে ("limit 3 বলে reject হয়েছে", শুধু "না" না)। Boolean precheck yes/no answer দেয়; Result কারণ বহন করে, আর compiler caller-কে ignore করতে দেয় না। Rust বিখ্যাতভাবে Result touch করা code compile করবে না যদি error arm handle না করা হয়, আর তার ? operator failure উপরে পাঠানোকে এক character-এ সমাধান করে। খরচও সৎভাবে বলি: exception-built language-এ প্রতিটা layer Result manually unwrap বা forward করতে হয়; library boundary এখনো throw করে, তাই wrap করতে হয়; আর half-adopted style confuse করে। Balanced modern recipe: simple presence question → precheck বা Try-method; কারণ সহ expected domain failure → Result type; genuine anomaly → exception। তিনটা tool, তিনটা কাজ, কোনো overlap নেই।
কোন smells এটা সারায়?
| Smell | এই refactoring কীভাবে সাহায্য করে |
|---|---|
| Exceptions as control flow | Directly anti-pattern সরায় — routine branching if-এ ফিরে আসে, reader দেখতে পায় |
| Long Method | পাঁচ লাইনের try/catch ceremony one-line guard-এ collapse করে, method অনেক ছোট হয় |
| Duplicate Code | প্রতিটা lookup-এ copy-paste করা identical catch-and-default block একটা shared guard বা helper হয় |
| Noisy log / alert fatigue | Expected condition আর stack trace generate করে না, তাই monitoring শুধু real anomaly দেখায় |
| Comments বলছে "এই exception normal" | Code এখন সরাসরি বলে — সৎ if-এর কোনো apology comment দরকার নেই |
Quick revision box
+------------------------------------------------------------------+
| REPLACE EXCEPTION WITH TEST (PRECHECK) - REVISION CARD |
+------------------------------------------------------------------+
| Problem : try/catch handling a condition that is ordinary, |
| predictable, and cheap to check (missing key, |
| empty list, unparseable input) |
| Solution : test the condition UP FRONT with a plain if; |
| keep exceptions for the truly unexpected |
| |
| ASK FIRST: |
| expected + cheap check + private state -> PRECHECK (this) |
| shared resource, state may change -> act + catch (race!) |
| no cheap check exists -> Try-method / Result |
| genuinely abnormal -> keep the EXCEPTION |
| |
| THE PAIR : this post = stop shouting about Tuesdays |
| twin post = start shouting about real fires |
| C# bonus : TryGetValue / TryParse = check + act, atomic, fast |
| (analyzer CA1854 finds double lookups for you) |
+------------------------------------------------------------------+নিজে practice করো
তোমার পালা। একটা school attendance app roll number দিয়ে students present mark করে, আর original author try/catch একটু বেশিই পছন্দ করতেন।
function markPresent(register: Map<number, Student>, roll: number): string {
try {
const student = register.get(roll)!;
student.presentDays += 1; // throws on undefined
return `Marked: ${student.name}`;
} catch {
return "Unknown roll number"; // expected: typos happen daily
}
}
function averageMarks(marks: number[]): number {
try {
return marks.reduce((a, b) => a + b) / marks.length; // reduce throws on []
} catch {
return 0; // expected: new student, no marks
}
}নিজে ধাপে ধাপে refactoring করো:
১. প্রতিটা function-এর জন্য এই sentence শেষ করো "আমরা catch-এ পৌঁছাই যখন ___" আর বলো সেই situation expected নাকি abnormal।
২. markPresent refactor করো upfront register.has(roll) check দিয়ে (বা get + undefined test দিয়ে)। Try/catch আর বিপজ্জনক ! সরাও।
৩. averageMarks refactor করো marks.length === 0 guard দিয়ে। Try/catch সরাও।
৪. প্রতিটা function-এর জন্য তিনটা test লেখো: normal case, absent/empty case, আর তোমার choice-এর একটা boundary।
৫. দুটো situation চিত্র ৯-এর quadrant chart-এ plot করো। দুটোই bottom-left-এ পড়া উচিত — এক sentence-এ explain করো কেন in-memory register map race-condition চিন্তাকে irrelevant করে।
৬. Bonus thinking: একজন teammate বলছে "চলো database save-ও precheck করি — if (db.isConnected()) db.save()।" দুই sentence-এ explain করো কেন এই precheck false comfort, আর সঠিক tool কী। Hint: সেই connection আর কে use করছে, আর check আর save-এর মাঝে কী হতে পারে?
৭. Stretch goal: markPresent redesign করো Result-style union return করতে — { ok: true; name: string } | { ok: false; reason: "unknown-roll" } — আর এক sentence-এ লেখো compiler এখন calling screen-কে কী করতে বাধ্য করে যেটা plain string return কখনো করতে পারত না।
যদি বন্ধুকে explain করতে পারো কেন canteen board watchman-এর চিৎকারের চেয়ে ভালো — আর কেন board সুমাইয়াকে protect করতে পারে না যখন সে হাঁটছে আর অন্য কেউ দোকান বন্ধ করে দিতে পারে — তুমি এই refactoring আর তার সীমাবদ্ধতা পুরোপুরি বুঝে গেছো।
সচরাচর জিজ্ঞাসা
- Replace Exception with Test মানে আসলে কী?
- মানে হলো — যদি কোনো 'failure' আসলে একটা স্বাভাবিক, predictable situation যেটা তুমি সহজেই আগেভাগে check করতে পারো — যেমন missing key, empty list, parseable string — তাহলে সেটা সামলাতে try/catch ব্যবহার না করে, কাজ করার আগেই একটা plain if দিয়ে check করো। Exception শুধু সত্যিকারের অপ্রত্যাশিত সমস্যার জন্য রেখে দাও।
- এই refactoring-এর কি আরেকটা নাম আছে?
- হ্যাঁ। Martin Fowler-এর Refactoring বইয়ের দ্বিতীয় edition-এ এটাকে Replace Exception with Precheck বলা হয়েছে — নামটা mechanics টা ভালোভাবে বোঝায়। তুমি risky operation-এর আগে একটা precheck যোগ করো। পুরনো sources আর refactoring.guru-তে পুরনো নাম Replace Exception with Test ব্যবহার হয়।
- কখন আগে থেকে test করাটা ভুল হবে?
- দুটো ক্ষেত্রে। প্রথমত, যখন check আর action-এর মাঝখানে state বদলে যেতে পারে — যেমন তুমি check করলে file আছে, তারপর অন্য একটা process সেটা delete করল, তারপর তুমি open করতে গেলে। সেখানে exception catch করাটাই একমাত্র atomic, সঠিক উপায়। দ্বিতীয়ত, যখন কোনো সস্তা side-effect-free check নেই, তখন test করতে গেলে operation দুইবার করতে হবে।
- Check করা কতটা দ্রুত exception throw করার চেয়ে?
- অনেক অনেক বেশি দ্রুত। Throw করলে stack trace capture হয় আর stack unwind হয় — এটা simple comparison-এর চেয়ে শত থেকে হাজার গুণ ধীর হতে পারে। Hot loop-এ — যেমন এক মিলিয়ন row parse করা — exceptions as control flow মিনিটের কাজকে ঘণ্টায় পরিণত করতে পারে।
- Result type এই ব্যাপারে কোথায় fit করে?
- Result বা Either type হলো তৃতীয় পথ — operation একটা object return করে যেটা হয় success with a value, নয়তো typed failure। এটা এমন expected failure-এর জন্য ভালো যেগুলো information বহন করে, আর compiler বাধ্য করে সেটা handle করতে। Simple presence check-এর জন্য upfront test ভালো; সত্যিকারের unexpected-এর জন্য exception; Result দুটোর মাঝখানে।
আরো দেখো
সম্পর্কিত পাঠ
Replace Error Code with Exception: ব্যর্থতাকে চুপিচুপি নয়, সরাসরি জানাও
Replace Error Code with Exception রিফ্যাক্টরিং শেখো একটা সরকারি অফিসের গল্পের মাধ্যমে — before/after TypeScript আর C# উদাহরণ, নিরাপদ migration ধাপ, আর Result type-এর সাথে সৎ তুলনাসহ।
Long Method: যখন একটা function সব কিছু করতে চায়
Long Method code smell শিখো সহজ গল্পের মাধ্যমে — TypeScript আর C# example সহ, Extract Method দিয়ে step-by-step refactoring। একদম beginner-friendly গাইড।
Duplicate Code: ৫০টা বিয়ের কার্ডে হাতে লেখা একই ঠিকানা
বিয়ের কার্ডের গল্প দিয়ে Duplicate Code smell বোঝো। DRY, Rule of Three, আর Extract Method দিয়ে copy-paste কোডের বিপদ থেকে বাঁচো।
Primitive Obsession: যখন সব কিছুই শুধু একটা string বা number
Primitive Obsession সহজ ভাষায় — কেন plain string আর number bug লুকিয়ে রাখে, আর কীভাবে Money বা Address-এর মতো value object দিয়ে code-কে safe আর পরিষ্কার করা যায়।