মূল বিষয়বস্তুতে যান
Clean Code Mastery

Replace Error Code with Exception: ব্যর্থতাকে চুপিচুপি নয়, সরাসরি জানাও

Replace Error Code with Exception রিফ্যাক্টরিং শেখো একটা সরকারি অফিসের গল্পের মাধ্যমে — before/after TypeScript আর C# উদাহরণ, নিরাপদ migration ধাপ, আর Result type-এর সাথে সৎ তুলনাসহ।

25 মিনিট আপডেট: June 11, 2026beginner
refactoringexceptionserror codeserror handlingresult typetypescriptcsharp

📢 যে বাবু ফিসফিস করেন, যে ম্যাডাম সরাসরি বলেন

ধরো তুমি একটা ব্যস্ত সরকারি অফিসে গেছো। সুমাইয়া, ফাইনাল ইয়ারের একজন ছাত্রী, স্কলারশিপের ফর্ম জমা দিতে এসেছে। সব কাগজ দুই কপি করে ফটোকপি করা, সকালের রোদে দাঁড়িয়ে চল্লিশ মিনিট লাইনে অপেক্ষা করেছে। অবশেষে কাউন্টার নম্বর ৩-এ ফর্মটা দিল — জামাল বাবুর কাছে, যিনি এই কাউন্টারে এগারো বছর ধরে আছেন। তার একটা অদ্ভুত অভ্যাস আছে।

ফর্মে কোনো সমস্যা থাকলে জামাল বাবু একটা কথাও বলেন না। ফর্মের নিচের কোণে পেন্সিল দিয়ে ছোট করে "-1" লিখে দেন, ফর্মটা হাতে ফিরিয়ে দেন, পরবর্তীজনকে ডাকেন। ব্যস। কোনো ঘোষণা নেই, কোনো ব্যাখ্যা নেই। এই "-1" হলো অফিসের ভেতরের গোপন কোড — শুধু কেরানিরা জানে। সুমাইয়া যদি এই গোপন কোড না জানে — আর সে জানে না — তাহলে বাড়ি ফিরে যায় আনন্দে, মনে করে স্কলারশিপ হয়ে গেছে।

তিন মাস পরে: স্কলারশিপ নেই, কোনো রেকর্ড নেই, কোনো সূত্র নেই। সুমাইয়া আবার আসে, একই লাইনে দাঁড়ায়। অবশেষে একজন সিনিয়র কেরানি ফর্মটা উল্টে দিয়ে ফিকে পেন্সিলের দাগটা দেখায়। "ছবি নেই। দেখো? মাইনাস এক।" মার্চে কাউন্টার ৩-এ ব্যর্থতা হয়েছিল, জুনে অন্য একটা শহরে সে জানল — আসল কারণ কবেই ভুলে গেছে, শেষ তারিখও চলে গেছে।

এবার সুমাইয়ার সাথে কাউন্টার ৭-এ যাও। সেখানে নাসরিন ম্যাডাম বসেন। তিনি আলাদা। যেই সমস্যা পান, ফর্মটা তুলে ধরে পুরো হলঘর শুনতে পাবে এভাবে স্পষ্ট জোরে বলেন: "ফর্ম বাতিল — ছবি নেই!" সবাই শুনতে পায়। কাউন্টার ৭ থেকে মনে করে বের হওয়ার কোনো উপায় নেই যে সব ঠিকঠাক। একটু লজ্জার? হয়তো। কিন্তু সুমাইয়া আজকেই ছবি ঠিক করে ফেলে, পাশের ফটোশপ থেকে, কারণ কারণটা এখনো একদম টাটকা।

পেন্সিলের "-1" হলো একটা error code। জোরে ঘোষণাটা হলো একটা exception। আজকের রিফ্যাক্টরিং, Replace Error Code with Exception, কাউন্টার ৩-কে কাউন্টার ৭-এর মতো করে তোলে — যাতে ব্যর্থতা আর কখনো চুপচাপ পেরিয়ে না যায়।

চিত্র ১: ফিসফিসানো কাউন্টার আর ঘোষণা করা কাউন্টারে সুমাইয়ার ফর্ম

journey score-গুলো ভালো করে দেখো। চুপ থাকা কাউন্টারটা মাঝখানে আসলে ভালো মনে হয় — সুমাইয়া তো বাড়ি যায় খুশি হয়ে! এটাই error code-কে এত বিপজ্জনক করে। ব্যর্থতার মুহূর্তটা ব্যথাহীন। ব্যথা আসে পরে, বড় হয়ে, কারণ থেকে অনেক দূরে। জোরে কাউন্টারে একটা অস্বস্তিকর মুহূর্ত, আর তারপর থেকে সব ভালো। Exception একটা ছোট তাৎক্ষণিক যন্ত্রণার বিনিময়ে নীরব বিপর্যয়ের অবসান ঘটায়।

📜 Replace Error Code with Exception কী জিনিস?

অনেক পুরনো codebase (আর অনেক C-style API) ব্যর্থতা জানায় একটা magic value return করে: -1, 0, null, false, বা কোনো numeric constant যেমন ERR_NO_FUNDS = 7। Method-এর একটাই return value দুটো কাজ করছে একসাথে — কখনো সত্যিকারের উত্তর, কখনো গোপন distress signal।

এই design-এর একটাই মারাত্মক দুর্বলতা: caller-কে দেখতে কেউ বাধ্য করে না। Code check করা ঐচ্ছিক। আর অনেক caller আছে এমন যেকোনো codebase-এ, "ঐচ্ছিক" মানে শেষমেশ "কোথাও না কোথাও ভুলে যাওয়া।" ভুলে যাওয়া check crash করে না। এটা তার থেকেও খারাপ কিছু করে — program-কে ভুল value নিয়ে শান্তভাবে চলতে দেয়। সেই value নিচে বয়ে যায়, save হয়, যোগ হয়, print হয়, যতক্ষণ না পুরোপুরি আলাদা কোনো module-এ কিছু ভেঙে পড়ে। তারপর debug করতে গেলে দৃশ্যমান ক্ষতি থেকে শুরু করে নীরব কারণ পর্যন্ত পিছিয়ে যেতে হয়, কখনো কয়েকদিনের log ধরে। প্রতিটা অভিজ্ঞ programmer একটা পেন্সিলের "-1"-এর কারণে একটা সপ্তাহান্ত নষ্ট করেছে।

Replace Error Code with Exception বলে: method ব্যর্থ হলে, code return করার বদলে exception throw করো। এই রিফ্যাক্টরিং সরাসরি Martin Fowler-এর Refactoring catalog থেকে এসেছে। Refactoring.guru-র treatment একটা গুরুত্বপূর্ণ কথা যোগ করেছে — error code return করা হলো procedural programming-এর অচল অবশিষ্ট, এমন দিনগুলোর থেকে যখন function-এর চিৎকার করার অন্য কোনো উপায় ছিল না।

একটা exception তিনভাবে ব্যর্থতার physics পরিবর্তন করে।

  1. এটাকে চুপচাপ উপেক্ষা করা যায় না। কেউ handle না করলে এটা call stack-এর উপরে উঠতে থাকে আর program-কে জোরে থামিয়ে দেয় — stack trace সহ, ঠিক সেই লাইনটা দেখায়। নাসরিন ম্যাডামের ঘোষণা, কিউ নম্বর সহ।
  2. এটা দুটো channel আলাদা করে। Return value এখন শুধু সত্যিকারের উত্তর বহন করে। ব্যর্থতা তার নিজস্ব dedicated path-এ যায়। আর "এটা কি -1 তাপমাত্রা নাকি error?" এমন confusion নেই।
  3. এটা সমৃদ্ধ context বহন করে। Exception হলো একটা পূর্ণ object: message, type, stack trace, আর যেকোনো custom field — account ID, পরিমাণ, timestamp। একটা -1 কিছুই বহন করে না। এটা এমনকি বলতে পারে না কোন ছবিটা নেই।
💡

এক লাইনে সারাংশ: error code হলো একটা ফিসফিসানি যা caller মিস করতে পারে; exception হলো একটা ঘোষণা যেটা দায়িত্বশীল কেউ সামলানোর আগ পর্যন্ত building-এর উপরে উঠতেই থাকে।

আরো এগিয়ে যাওয়ার আগে, failure-reporting style-এর পুরো দুনিয়াটা একবার দেখে নাও।

চিত্র ২: error reporting-এর চারটি পরিবার — এই রিফ্যাক্টরিং তোমাকে প্রথম শাখা থেকে দ্বিতীয়তে নিয়ে যায়

আর এখানে সরাসরি তুলনার টেবিল — viva আর interview-এ কাজে আসবে।

প্রশ্নError code (-1, null, false)Exception
Caller কি এটা উপেক্ষা করতে পারে?হ্যাঁ — চুপচাপ, দুর্ঘটনাক্রমে, চিরকালের জন্যনা — unhandled মানে stack trace সহ জোরে থামা
উপেক্ষিত ব্যর্থতা কোথায় দেখা যায়?অনেক পরে downstream-এ, অসংশ্লিষ্ট code-এঠিক সেই লাইনে, সাথে সাথে
ব্যর্থতার সাথে কী তথ্য যায়?একটা magic value, আর কিছু নাType, message, stack trace, custom field
Return value-এর কী হয়?Hijack হয় — উত্তর আর signal একসাথে বহন করেমুক্ত — শুধু সৎ উত্তর বহন করে
মাঝের layer-গুলো কী করে?Numbering system-এর মধ্যে code translate করেকিছুই না — exception নিজের channel-এ pass হয়
Happy path-এ খরচ?কিছুই নাকিছুই না (আধুনিক runtime শুধু throw-এর সময় দাম নেয়)
Failure path-এ খরচ?একটা comparisonব্যয়বহুল — বিরল ঘটনার জন্য ঠিক, routine-এর জন্য ভুল

শেষ row-টা সৎ, আর আমরা এতে ফিরে আসব। Exception হলো অপ্রত্যাশিতের জন্য। প্রত্যাশিত আর routine-এর জন্য আলাদা tool দরকার — এই পোস্টের যমজ, Replace Exception with Test, সেটা cover করে।

🔍 কখন এটা দরকার?

তোমার code-এ এই লক্ষণগুলো খোঁজো।

  • Sentinel return value। Method-গুলো -1, null, false, বা STATUS_FAILED-এর মতো constant return করছে "এটা ভুল হয়েছে" বলতে। বিশেষত বিপজ্জনক যখন method আবার সত্যিকারের number return করে — তাপমাত্রা কি -1 হতে পারে? তাহলে -1 মানে error হতে পারে না।
  • Check করতে ভুলে যাওয়া caller। এমন method-এর call site খোঁজো। যদি একজন caller-ও error value-এর বিপরীতে compare না করে return value ব্যবহার করে, তোমার কাছে একটা জীবন্ত bug আছে যেটা তার মুহূর্তের অপেক্ষায়।
  • Happy path-এর সাথে জড়িয়ে যাওয়া error handling। if (result == -1) ... else if (result == -2) ...-এর সিঁড়ি normal logic-এর সাথে মিশে method দীর্ঘ আর অপাঠ্য হয়ে যায়।
  • কারণ থেকে অনেক দূরে আবিষ্কৃত ব্যর্থতা। "রিপোর্টে ১ কোটি টাকা মাইনাস দেখাচ্ছে" এমন bug report মানে একটা sentinel অনেক আগে data-তে ঢুকে গেছে।
  • Failure kind document করার magic constant। const ERR_NO_BALANCE = 1; const ERR_FROZEN = 2; — integer-এর একটা সমান্তরাল মহাবিশ্ব যেটা শুধু মূল লেখক decode করতে পারেন। এটা হলো Primitive Obsession smell error-handling পোশাক পরে।

যখন স্কলারশিপ অফিস তার form-processing software audit করল, team গণল codebase জুড়ে ব্যর্থতা কীভাবে জানানো হয়েছে। ছবিটা সুন্দর ছিল না।

চিত্র ৩: রিফ্যাক্টরিংয়ের আগে legacy codebase-এ কীভাবে ব্যর্থতা জানানো হতো

অর্ধেক ব্যর্থতা ছিল পেন্সিলের দাগ। আরো এক তৃতীয়াংশ ছিল console.log("save failed") লাইন — খালি ঘরে করা ঘোষণা, কারণ production server-এর console কেউ দেখে না। পাঁচটির মধ্যে মাত্র একটা ব্যর্থতা আসলে program থামিয়ে মনোযোগ দাবি করত।

একটু ভাবো — আর একটা লক্ষণ যেখানে থামা উচিত: "ব্যর্থতা" কি আসলে প্রত্যাশিত আর routine? যে search কিছু খুঁজে পায় না সেটা ব্যর্থতা না। User phone number field-এ অক্ষর টাইপ করা মঙ্গলবারের ঘটনা, বিপর্যয় না। সেই ক্ষেত্রে exception ভুল loudspeaker। যমজ রিফ্যাক্টরিং Replace Exception with Test দেখো — এই দুটো রিফ্যাক্টরিং একটা জুটি, আর এদের মধ্যে বেছে নেওয়াটাই আসল দক্ষতা।

⚖️ Before আর After এক নজরে

এখানে স্কলারশিপ অফিসের banking module, আগে। ব্যর্থতা একটা পেন্সিলের দাগ।

// BEFORE: -1 means failure... if anyone remembers to look
const ERR_INSUFFICIENT = -1;
 
function withdraw(account: Account, amount: number): number {
  if (amount > account.balance) {
    return ERR_INSUFFICIENT; // the whisper
  }
  account.balance -= amount;
  return 0; // success... also a magic number
}
 
// Caller A remembers the secret code:
if (withdraw(acc, 500) === ERR_INSUFFICIENT) {
  notifyUser("Not enough balance");
}
 
// Caller B forgot. The failure vanishes. Balance is wrong forever.
withdraw(acc, 99999);
sendReceipt(acc); // happily sends a receipt for money that never moved

রিফ্যাক্টরিংয়ের পরে, ব্যর্থতা নিজেই ঘোষণা করে।

// AFTER: failure is thrown, not returned
class InsufficientFundsError extends Error {
  constructor(
    readonly accountId: string,
    readonly requested: number,
    readonly available: number,
  ) {
    super(
      `Account ${accountId}: requested ${requested}, ` +
      `only ${available} available`,
    );
    this.name = "InsufficientFundsError";
  }
}
 
function withdraw(account: Account, amount: number): void {
  if (amount > account.balance) {
    throw new InsufficientFundsError(account.id, amount, account.balance);
  }
  account.balance -= amount;
}
 
// Caller A handles it explicitly:
try {
  withdraw(acc, 500);
} catch (e) {
  if (e instanceof InsufficientFundsError) {
    notifyUser(`Not enough balance: you have ${e.available}`);
  } else {
    throw e; // not ours — let it travel upward
  }
}
 
// Caller B "forgot" again — but now the program STOPS at this line,
// with a stack trace, before any wrong receipt is sent.
withdraw(acc, 99999);

গভীর পরিবর্তনগুলো লক্ষ্য করো। Return type void হয়ে গেল — function আর ভান করছে না যে তার return value একটা status report। Error object account ID আর দুটো amount বহন করে, তাই যে catch করে সে একটা কাজের message লিখতে বা log করতে পারে। আর ভুলে যাওয়া caller আর নীরব data-corruption bug না — এটা এখন অপরাধের আসল স্থানে একটা জোরে, তাৎক্ষণিক, debuggable crash।

চিত্র ৪: উপেক্ষিত error code চুপচাপ data-তে ঢুকে যায়; unhandled exception আসল কারণেই থামে

Exception কী কিনে দেয় সেটা state-machine দিয়েও দেখা যায়। একটা thrown rejection-এর সাথে, একটা ফর্ম ভাঙা অবস্থায় accepted state-এ যেতে পারে না — Rejected থেকে বের হওয়ার একমাত্র পথ হলো সমস্যা ঠিক করা।

চিত্র ৫: exception-এর সাথে, একটা বাতিল ফর্ম চুপচাপ এগোতে পারে না — সামনে যাওয়ার একমাত্র পথ fix করা

এটাকে error-code দুনিয়ার সাথে তুলনা করো, যেখানে Rejected থেকে সরাসরি Accepted-এ একটা অদৃশ্য extra arrow আছে — যেটা চলে যখনই কোনো caller check করতে ভুলে। পুরো রিফ্যাক্টরিংটা এভাবে বলা যায়: অদৃশ্য arrow মুছে দাও।

🪜 ধাপে ধাপে, নিরাপদ পদ্ধতিতে

একটা method কীভাবে ব্যর্থতা জানায় সেটা পরিবর্তন করলে প্রতিটি caller-কে স্পর্শ করে। তাই আমরা ধীরে যাই।

ধাপ ১: ব্যর্থতা সত্যিই exceptional কিনা সিদ্ধান্ত নাও। এটাই সেই ধাপ যেটা বেশিরভাগ মানুষ এড়িয়ে যায়, আর এটাই সবচেয়ে গুরুত্বপূর্ণ। জিজ্ঞেস করো: এই outcome কি বিরল আর অস্বাভাবিক (overdraft, corrupted record, ভাঙা invariant)? তাহলে এগিয়ে যাও। এটা কি routine আর প্রত্যাশিত (item পাওয়া যায়নি, খালি input)? তাহলে থামো — তুমি হয়তো Try... method বা Result type চাও, exception না।

ধাপ ২: context বহন করে এমন specific exception type তৈরি করো। শুধু Error("failed") না। Handler-এর যে data দরকার হবে সেটা দাও — account ID, চাওয়া আর available পরিমাণ, field name। নাসরিন ম্যাডাম "ফর্ম বাতিল!" চিৎকার করেন না; তিনি চিৎকার করেন "ফর্ম বাতিল — ছবি নেই।"

ধাপ ৩: প্রথমে safety check-এর পেছনে code-এর পাশাপাশি throw করো। একটা সুন্দর মধ্যবর্তী কৌশল: error code return করতে থাকো, কিন্তু unmigrated caller-দের জন্য পুরনো নামে একটা সাময়িক wrapper রাখো।

// INTERMEDIATE: new throwing version + old wrapper kept temporarily
function withdrawOrThrow(account: Account, amount: number): void {
  if (amount > account.balance) {
    throw new InsufficientFundsError(account.id, amount, account.balance);
  }
  account.balance -= amount;
}
 
/** @deprecated migrate to withdrawOrThrow */
function withdraw(account: Account, amount: number): number {
  try {
    withdrawOrThrow(account, amount);
    return 0;
  } catch (e) {
    if (e instanceof InsufficientFundsError) return -1;
    throw e;
  }
}

পুরনো caller-রা compile হতে থাকে। নতুন code সৎ version ব্যবহার করে। তুমি নিজের গতিতে migrate করো, test সবুজ থাকে।

ধাপ ৪: একে একে caller migrate করো। প্রতিটি call site-এ, === -1 check-কে try/catch দিয়ে replace করো — বা অনেক সময় আরো ভালো, কিছুই না: এই caller যদি ব্যর্থতা meaningfully handle করতে না পারে, exception-কে উপরে এমন layer-এ যেতে দাও যেটা পারে। Building-এর প্রতিটি তলায় নিজের announcement desk থাকার দরকার নেই।

ধাপ ৫: wrapper, error constant মুছে ফেলো, return type ঠিক করো। যখন "find usages" দেখায় পুরনো withdraw unused, সেটা সরাও। ERR_INSUFFICIENT সরাও। Method এখন শুধু meaningful result return করে — প্রায়ই void

ধাপ ৬: দুটো path-ই test করো। Happy withdrawal-এর জন্য একটা test, আর overdraft-এ সঠিক exception (ভেতরে সঠিক data সহ) throw হয় কিনা assert করার একটা test।

⚠️

Migrate করার সময় দুটো ফাঁদ এড়িয়ে চলো। প্রথম, খালি catch block: catch (e) {} মূল silent-failure সমস্যাটাকে আবার তৈরি করে — যা handle করতে পারো না তা গিলে ফেলো না। দ্বিতীয়, catch-everything block: catch (Exception) null-reference bug আর typo ধরে সেগুলোকে "insufficient funds" বলে report করবে। শুধু সেই specific type catch করো যেটা তুমি বোঝো; বাকিগুলো rethrow বা ignore করো যাতে real bug দৃশ্যমান থাকে। আর যখন একটা exception অন্যটায় wrap করো, সবসময় মূলটা inner exception হিসেবে যোগ করো যাতে stack trace টিকে থাকে।

কলেজের কোণা: একটা throw আসলে কত খরচ করে? ছাত্ররা "exception ধীর" আর "exception বিনামূল্যে" দুটো কথাই আলাদা আলাদা professor-এর কাছে শোনে — দুটোই অর্ধেক সত্য। আধুনিক runtime (JVM, .NET, table-based unwinding সহ C++) happy path-এ zero-cost exception handling implement করে: একটা try block-এ ঢোকা মূলত কিছুই করে না, কারণ handler location-গুলো static table-এ থাকে যেটা শুধু throw হলে দেখা হয়। বিল আসে throw-এর সময়: runtime stack trace capture করে, frame by frame unwind table দেখে, cleanup code চালায়, handler type match করে — কাজ microsecond-এ মাপা হয়, plain return-এর nanosecond-এর বিপরীতে। শত থেকে হাজার গুণ পার্থক্য। নাসরিন ম্যাডামের জন্য এটা সঠিক trade: ফর্ম reject করা বিরল, তাই microsecond কিছুই না। কিন্তু million-row parsing loop-এর ভেতরে throw দাও আর import সেকেন্ড থেকে মিনিটে পরিণত হয়। যে engineering rule বের হয়: frequency tool নির্ধারণ করে। বিরল আর অস্বাভাবিক → throw করো। ঘনঘন আর প্রত্যাশিত → আগেভাগে check করো বা Result return করো।

🏢 একটা বড় real-life উদাহরণ

চলো স্কলারশিপ অফিসটাই follow করি। Form-processing service layered error code ব্যবহার করত — আর code-এর উপর code ডাকা মানে এই design সত্যিই ভেঙে পড়ে। Building-এর প্রতিটি তলাকে অন্য প্রতিটি তলার গোপন পেন্সিলের দাগ শিখতে হয়।

// BEFORE: every layer translates the lower layer's codes. Madness grows.
function validatePhoto(form: Form): number {
  if (!form.photo) return 1;            // 1 = missing
  if (form.photo.sizeKb > 200) return 2; // 2 = too big
  return 0;
}
 
function processForm(form: Form): number {
  const photoResult = validatePhoto(form);
  if (photoResult === 1) return 101;     // translate: 1 -> 101
  if (photoResult === 2) return 102;     // translate: 2 -> 102
  if (!form.signature) return 103;
  saveToRegistry(form);                  // returns its own codes... ignored!
  return 0;
}
 
// UI layer decodes a third numbering system:
const code = processForm(form);
if (code === 101) show("Photo missing");
else if (code === 102) show("Photo too large");
else if (code === 103) show("Signature missing");

সমস্যাগুলো গণো। তিনটে numbering system sync রাখতে হবে। saveToRegistry return code চুপচাপ বাদ দেওয়া হয়েছে — একটা real bug, অদৃশ্য, "error handling"-এর মাঝখানে বসে আছে। প্রতিটি function-এর অর্ধেকটাই শুধু সংখ্যা উপরে বহন করার plumbing। এখন exception version:

// AFTER: each problem is a typed announcement; middle layers carry nothing
class FormRejectedError extends Error {
  constructor(readonly reason: string, readonly field: string) {
    super(`Form rejected — ${reason}`);
    this.name = "FormRejectedError";
  }
}
 
class RegistryError extends Error {
  constructor(readonly formId: string, cause: unknown) {
    super(`Registry save failed for ${formId}`, { cause });
    this.name = "RegistryError";
  }
}
 
function validatePhoto(form: Form): void {
  if (!form.photo)
    throw new FormRejectedError("photograph missing", "photo");
  if (form.photo.sizeKb > 200)
    throw new FormRejectedError("photograph larger than 200 KB", "photo");
}
 
function processForm(form: Form): void {
  validatePhoto(form);              // no translation layer at all
  if (!form.signature)
    throw new FormRejectedError("signature missing", "signature");
  saveToRegistry(form);             // its failure now travels up too
}
 
// UI layer — the one place that talks to the user:
try {
  processForm(form);
  show("Form accepted!");
} catch (e) {
  if (e instanceof FormRejectedError) {
    show(e.message);                // "Form rejected — photograph missing"
    highlightField(e.field);
  } else {
    show("Something went wrong. Please try again.");
    reportToSupport(e);             // RegistryError and unknowns go here
  }
}

Middle layer তার আসল logic-এ সংকুচিত হয়েছে। তিনটে numbering dictionary চলে গেছে। বাদ দেওয়া saveToRegistry ব্যর্থতা এখন বাদ দেওয়া অসম্ভব। আর UI ঠিক সেই field highlight করতে পারছে, কারণ exception সেটা বহন করছে

চিত্র ৬: exception-এর সাথে, middle layer failure code translate করা বন্ধ করে — ঘোষণা নিজের channel-এ উপরে যায়

Exception-গুলো নিজেরাই একটা ছোট design deserve করে। এগুলো একটা class hierarchy, আর hierarchy-টাই তোমার catch-policy: FormRejectedError catch করো যেখানে user-এর সাথে কথা বলো, RegistryError catch করো যেখানে support-এর সাথে কথা বলো, বাকি সব top-level handler-এ যেতে দাও।

চিত্র ৭: exception পরিবার — hierarchy হলো ব্যর্থতার routing plan

💼 C#-এ একই রিফ্যাক্টরিং

C# এই philosophy তার হাড়ে নিয়ে তৈরি হয়েছিল। এখানে withdrawal, .NET উপায়ে।

public class InsufficientFundsException : Exception
{
    public string AccountId { get; }
    public decimal Requested { get; }
    public decimal Available { get; }
 
    public InsufficientFundsException(
        string accountId, decimal requested, decimal available)
        : base($"Account {accountId}: requested {requested:C}, " +
               $"only {available:C} available")
    {
        AccountId = accountId;
        Requested = requested;
        Available = available;
    }
}
 
public class BankService
{
    public void Withdraw(Account account, decimal amount)
    {
        if (amount > account.Balance)
            throw new InsufficientFundsException(
                account.Id, amount, account.Balance);
        account.Balance -= amount;
    }
}
 
// Caller — catch exactly what you can handle:
try
{
    bank.Withdraw(account, 500m);
}
catch (InsufficientFundsException ex)
{
    Console.WriteLine($"Sorry — only {ex.Available:C} available.");
}

তিনটে .NET-specific নোট। প্রথম, সরাসরি Exception থেকে derive করো আর class নামের শেষে Exception লাগাও — এটাই framework convention। দ্বিতীয়, catch block-এর ভেতরে rethrow করার সময় throw; লেখো, throw ex; না — দ্বিতীয় form মূল stack trace নষ্ট করে, exception যে প্রমাণ রক্ষার জন্য আছে সেটাই মুছে দেয়। তৃতীয়, service boundary-তে exception-কে translate করতে হবে, wire পেরিয়ে throw করা নয়: একটা ASP.NET API InsufficientFundsException-কে problem-details body সহ HTTP 422-এ map করে, কারণ HTTP status code বলে, .NET object নয়। Protocol boundary-তে error code ঠিক আছে আর স্বাভাবিক — এই রিফ্যাক্টরিং তোমার program-এর ভেতরের error code-কে target করে।

সম্পূর্ণতার জন্য, Python-এ একই ধারণা, যেখানে custom exception class দুই লাইন আর ভাষার সংস্কৃতি strongly sentinel return করার চেয়ে raise করাকে সমর্থন করে:

class InsufficientFundsError(Exception):
    def __init__(self, account_id: str, requested: float, available: float):
        super().__init__(
            f"Account {account_id}: requested {requested}, only {available} available"
        )
        self.account_id = account_id
        self.requested = requested
        self.available = available
 
 
def withdraw(account, amount: float) -> None:
    if amount > account.balance:
        raise InsufficientFundsError(account.id, amount, account.balance)
    account.balance -= amount
 
 
# The caller catches exactly what it understands:
try:
    withdraw(acc, 500)
except InsufficientFundsError as err:
    notify_user(f"Not enough balance: you have {err.available}")

কলেজের কোণা: checked exception বিতর্ক। Java checked exception দিয়ে "caller জানে না এটা fail করতে পারে" সমস্যা সমাধান করার চেষ্টা করেছিল: একটা method throws InsufficientFundsException declare করে, আর compiler প্রতিটি caller-কে হয় catch করতে অথবা এগিয়ে declare করতে বাধ্য করে। কাগজে কলমে দারুণ — অদৃশ্য failure channel signature-এ দৃশ্যমান হয়, compiler নাসরিন ম্যাডাম হয়ে যায়। কিন্তু বাস্তবে, Java-র পঁচিশ বছর শিল্পকে খরচ শেখাল: নিচের প্রতিটি জিনিস re-declare করতে করতে signature ফুলে ওঠে; অলস programmer empty catch block দিয়ে compiler চুপ করায় — নতুন করে নীরব পেন্সিলের দাগ, এখন compiler-এর অনুমোদন সহ; আর checked exception lambda আর stream-এর সাথে ভয়ংকরভাবে কাজ করে। C# এসব দেখল আর ইচ্ছাকৃতভাবে শুধু unchecked exception নিয়ে launch করল। Kotlin আর Swift আলাদা আলাদা মধ্যম ভূমিতে পৌঁছাল, আর আধুনিক functional উত্তর হলো প্রত্যাশিত ব্যর্থতা return type-এ রাখা — যেটাই নিচের Result-type ধারণা। Exam-এর জন্য সারাংশ: checked exception signature-এ failure channel দৃশ্যমান করার চেষ্টা করল; ধারণাটা সঠিক ছিল, ergonomics ভুল ছিল, আর Result type হলো সেই একই ধারণার আধুনিক পুনরায় চেষ্টা।

🛠️ IDE সাপোর্ট

কোনো major IDE-তে এক-ক্লিকে "Replace Error Code with Exception" নেই — এই রিফ্যাক্টরিং behaviour contract পরিবর্তন করে, তাই একটা মানুষের মাথা দরকার। কিন্তু tooling এখনো অনেক সাহায্য করে।

  • Find Usages (IntelliJ, Rider, VS Code, Visual Studio) তোমাকে error code-এর বিপরীতে compare করা সব call site-এর সম্পূর্ণ checklist দেয় — ধাপ ৪-এর জন্য তোমার migration list।
  • Change Signature রিফ্যাক্টরিং return type (যেমন int থেকে void) সব caller-এ একটি নিরাপদ operation-এ update করে।
  • Live template / snippet ভালোভাবে গঠিত custom exception class তৈরি করে (Rider আর ReSharper-এ exc template)।
  • Static analyzer হলো তোমার পরবর্তী watchdog: SonarLint empty catch block আর control flow-এর জন্য exception ব্যবহার flag করে; .NET analyzer অতিরিক্ত broad catch (CA1031) আর swallowed exception সম্পর্কে সতর্ক করে; ESLint-এর no-empty আর @typescript-eslint rule নীরব catch {} block ধরে।
  • Compiler সাহায্য: TypeScript-এ, return type number থেকে void-এ পরিবর্তন করলে প্রতিটি বাকি === -1 comparison compile error হয় — unmigrated caller-দের বিনামূল্যে, সম্পূর্ণ list।

⚖️ সুবিধা আর ঝুঁকি

এখানে সৎ ledger — আর বড় ছবি, কারণ এই পোস্ট আর Replace Exception with Test একটা পাঠের দুটো অর্ধেক: অপ্রত্যাশিত সমস্যার জন্য exception, প্রত্যাশিত condition-এর জন্য আগেভাগে check।

সুবিধাঝুঁকি / খরচ
ব্যর্থতা চুপচাপ উপেক্ষা করা যায় না — কেউ handle করার আগ পর্যন্ত propagate হয়Throw করা ব্যয়বহুল (প্রায়ই return করার চেয়ে শত গুণ ধীর) — routine, frequent outcome-এর জন্য ভুল tool
Happy-path code পরিষ্কার পড়ে, আর if (code == ...) ladder-এর সাথে আর জড়ানো নেইException signature-এ অদৃশ্য (TS/C#-এ) — doc না পড়লে caller দেখতে পায় না কী throw হতে পারে
ব্যর্থতার সাথে সমৃদ্ধ context যায়: message, custom field, stack traceঅতিরিক্ত broad catch block অসংশ্লিষ্ট bug business handling-এর আড়ালে লুকাতে পারে
Middle layer সংকুচিত হয় — তলার মাঝে কোনো code-translation plumbing নেইException ordinary control flow হিসেবে ব্যবহার করা একটা anti-pattern যেটা আসল program flow লুকায়
Return value আবার একটাই সৎ অর্থ ফিরে পায়Process/HTTP boundary পেরিয়ে exception যাইহোক status code-এ map করতে হয়

স্কলারশিপ অফিসের রিফ্যাক্টরিং আসলে ফলপ্রসূ হয়েছিল কিনা? Team ছয় মাস একটা metric track করল: ব্যর্থতা যেগুলো ঘটার সময় কেউ না জেনে production data-তে পৌঁছাল।

চিত্র ৮: রিফ্যাক্টরিংয়ের আগে আর পরে প্রতি মাসে production-এ পৌঁছানো নীরব ব্যর্থতা

মাসে নয়টা নীরব ব্যর্থতা একটায় নেমে এল। আর সেই একটা এসেছিল একটা বাকি catch (e) {} block থেকে যেটা কেউ "লাল squiggle সরাতে" লিখেছিল — উপরের warning callout যে কেন তার জায়গাটা deserve করে সেটা প্রমাণ।

তাহলে প্রতিটি ব্যর্থতার জন্য তুমি কীভাবে সিদ্ধান্ত নেবে যে এটা exception deserve করে? দুটো প্রশ্ন এটা ঠিক করে: condition কি প্রত্যাশিত নাকি সত্যিই অস্বাভাবিক? আর caller কি সস্তায় আগে থেকে এটা test করতে পারে? যেকোনো ব্যর্থতা সেই দুটো axis-এ plot করো আর উত্তর chart থেকে পড়ো।

চিত্র ৯: দুটো প্রশ্ন tool নির্ধারণ করে — প্রত্যাশিততা আর testability

আর এখন তৃতীয় পথ, কারণ আধুনিক দুনিয়া একটা offer করে। Result type — Rust-এর built-in Result<T, E>, TypeScript-এ fp-ts-এর Either, আর C# library যেমন OperationResult বা ErrorOr — এমন একটা object return করে যেটা explicitly হয় success value অথবা error ধরে রাখে। Compiler তারপর caller-কে error case handle না করে value স্পর্শ করতে দেয় না। Rust-এ এটা বিখ্যাতভাবে কঠোর: Err arm উপেক্ষা করা code simply compile হয় না।

Result type প্রত্যাশিত ব্যর্থতার জন্য দুই দুনিয়ার সেরাটা একত্রিত করে: exception-এর মতো জোরালো (type system একটা সিদ্ধান্ত বাধ্য করে) কিন্তু return value-এর মতো সস্তা (কোনো stack unwinding নেই)। নতুন ভাষাগুলো লক্ষ্য করল — Rust আর Go একেবারে exception ছাড়াই launch করেছে। সৎ balance sheet: Result exception-এর চারদিকে তৈরি ভাষায় ceremony যোগ করে, প্রতিটি layer সেগুলো explicitly পাস করতে হয়, আর অর্ধেক-গৃহীত Result style (কিছু function throw করে, কিছু Result return করে) যেকোনো pure style-এর চেয়ে বেশি confusing। আজকের TypeScript/C# codebase-এর জন্য practical নিয়ম: সত্যিকারের exceptional-এর জন্য exception, প্রত্যাশিত domain failure-এর জন্য Result type বা Try-method, আর কখনোই bare error code না।

🧹 কোন smell-গুলো এটা সারায়?

Smellএই রিফ্যাক্টরিং কীভাবে সাহায্য করে
Error code / magic sentinel valueসরাসরি সরিয়ে দেয় — ব্যর্থতা তার নিজস্ব typed channel পায়
Primitive Obsessionঅর্থ-বোঝাই integer (-1, ERR_FROZEN = 2) named field সহ একটা real class হয়
Long MethodLogic-এর সাথে মিশে থাকা status-checking ladder মিলিয়ে যায়; method শুধু তার আসল কাজ রাখে
Duplicate Codeপ্রতিটি call site-এ copy-paste করা একই if (result == -1) check সঠিক level-এ একটা handler দিয়ে replace হয়
Switch StatementsNumeric error kind decode করা switch হয় typed catch block হয় — বা upward propagation-এ মিলিয়ে যায়

📋 দ্রুত revision box

+------------------------------------------------------------------+
|   REPLACE ERROR CODE WITH EXCEPTION - REVISION CARD              |
+------------------------------------------------------------------+
| Problem  : method returns -1 / null / false to signal failure    |
|            -> checking is voluntary -> someone forgets ->        |
|            wrong data travels silently, breaks far away          |
| Solution : THROW a specific exception carrying context;          |
|            return value keeps only its honest meaning            |
|                                                                  |
| RULES OF THE ANNOUNCEMENT:                                       |
|   - specific exception type, rich fields, clear message          |
|   - never swallow: empty catch = the old bug, fancier            |
|   - catch only what you can handle; let the rest travel          |
|   - C#: rethrow with "throw;" never "throw ex;"                  |
|                                                                  |
| THE PAIR : unexpected problem  -> EXCEPTION   (this post)        |
|            expected condition  -> TEST first  (twin post)        |
| THIRD WAY: Result / Either types - compiler-checked, cheap,      |
|            great for expected domain failures                    |
+------------------------------------------------------------------+

✏️ অনুশীলনের কাজ

এবার তোমার পালা। ধরো একটা library app error code দিয়ে book borrowing manage করছে — আর এতে ইতিমধ্যে একটা নীরব bug আছে।

const ERR_NOT_FOUND = -1;
const ERR_ALREADY_BORROWED = -2;
const ERR_LIMIT_REACHED = -3;
 
function borrowBook(memberId: string, bookId: string): number {
  const book = findBook(bookId);
  if (!book) return ERR_NOT_FOUND;
  if (book.borrowedBy) return ERR_ALREADY_BORROWED;
  if (countBorrowed(memberId) >= 3) return ERR_LIMIT_REACHED;
  book.borrowedBy = memberId;
  return 0;
}
 
// Somewhere in the UI:
borrowBook(member.id, selectedBook.id);  // return value ignored!
showMessage("Book issued successfully"); // ...even when it wasn't

রিফ্যাক্টরিং নিজে করো, ধাপে ধাপে:

  1. প্রথমে চিন্তার ধাপ: তিনটে ব্যর্থতার মধ্যে কোনগুলো সত্যিই exceptional, আর কোনগুলো প্রত্যাশিত user-facing outcome? প্রতিটির জন্য এক বাক্য লেখো। (Hint: একজন member ইতিমধ্যে borrowed book বাছা library-তে বেশ স্বাভাবিক।)
  2. BookNotFoundError, AlreadyBorrowedError (কে borrowed তা বহন করে), আর BorrowLimitError (limit বহন করে) তৈরি করো। borrowBook-কে throw করাও আর void return করাও।
  3. UI bug ঠিক করো: call-টা try/catch-এ মোড়াও, প্রতিটি error type-এর জন্য একটা specific friendly message দেখাও, আর success message শুধু success-এ দেখাও।
  4. জয় নিশ্চিত করো: এক বাক্যে ব্যাখ্যা করো কেন "Book issued successfully" মিথ্যাটা এখন দুর্ঘটনাক্রমে লেখা অসম্ভব।
  5. তিনটে ব্যর্থতা চিত্র ৯-এর quadrant chart-এ plot করো। সব কি "throw" quadrant-এ পড়ে, নাকি ধাপ ১ ইতিমধ্যে বলে দিয়েছে কিছু অন্য জায়গায় belongs?
  6. Bonus চিন্তা: borrowBook আরেকবার একটা Result-style union return করতে লেখো — { ok: true } | { ok: false; reason: "not-found" | "already-borrowed" | "limit" } — আর লক্ষ্য করো কোন version UI programmer-কে প্রতিটি case handle করতে বাধ্য করে। এই app-এর জন্য তুমি কোন style বেছে নেবে, আর কেন?
  7. Stretch goal: যমজ রিফ্যাক্টরিং Replace Exception with Test পড়ো আর সিদ্ধান্ত নাও — barcode scan করার সময় "book not found" কি exception হওয়া উচিত নাকি আগেভাগে check? দুই বাক্যে তোমার উত্তর defend করো।

একটু ভাবো — যদি তুমি একজন বন্ধুকে বোঝাতে পারো কেন একটা উপেক্ষিত -1 crash-এর চেয়ে বেশি বিপজ্জনক — কেন সুমাইয়া কাউন্টার ৭-এ একটু লজ্জা পাওয়া থেকে ভালো অবস্থায় ছিল কাউন্টার ৩ থেকে খুশি হয়ে বাড়ি ফেরার চেয়ে — তুমি এই রিফ্যাক্টরিং সম্পূর্ণভাবে বুঝে গেছো।

সচরাচর জিজ্ঞাসা

error code আসলে কী জিনিস?
error code হলো একটা বিশেষ return value — মাইনাস ওয়ান, null, false, বা কোনো numeric constant — যেটা গোপনে বলে কাজটা ব্যর্থ হয়েছে। বিপদ হলো, caller-কে এটা দেখতে কেউ বাধ্য করে না। কেউ একবার check করতে ভুলে গেলে, ব্যর্থতাটা চুপচাপ পুরো program-এর ভেতর দিয়ে বয়ে যায়, আর আসল কারণ থেকে অনেক দূরে গিয়ে সব ভেঙে পড়ে।
কখন error code-এর বদলে exception ব্যবহার করব?
যখন ব্যর্থতাটা সত্যিই অপ্রত্যাশিত আর কোনোভাবে এড়ানো যাবে না — যেমন ভাঙা invariant, থাকার কথা কিন্তু নেই এমন file, বা overdraft। unhandled exception প্রোগ্রামকে সঠিক জায়গায় জোরে থামিয়ে দেয়, কিন্তু unchecked error code চুপচাপ data নষ্ট করে।
exception কি অনেক ধীর?
exception throw করা value return করার চেয়ে অনেক ব্যয়বহুল — প্রায়ই শত থেকে হাজার গুণ। বিরল, সত্যিই অস্বাভাবিক ব্যর্থতার জন্য এই খরচ কোনো ব্যাপারই না। কিন্তু routine path-এ throw করলে সমস্যা। প্রত্যাশিত ও ঘনঘন হওয়া condition-এর জন্য আগেভাগে check করো বা Result type ব্যবহার করো।
Result type কী আর এটা কি দুটো option-এর চেয়ে ভালো?
Result type, যেমন Rust-এর Result, fp-ts-এর Either, বা C#-এর Result library, এমন একটা object যেটা explicitly হয় success value অথবা error ধরে রাখে। Compiler caller-কে দুটো case-ই handle করতে বাধ্য করে — exception-এর মতো জোরালো কিন্তু return value-এর মতো সস্তা। প্রত্যাশিত ব্যর্থতার জন্য দারুণ, তবে বাড়তি কাজ যোগ করে আর পুরো team একসাথে মেনে চললে সবচেয়ে ভালো কাজ করে।
base Exception type catch করা কি ঠিক আছে?
সাধারণত না। সবকিছু catch করলে null reference-এর মতো অসংশ্লিষ্ট bug তোমার business handling-এর আড়ালে লুকিয়ে যেতে পারে। শুধু সেই specific exception type-গুলোই catch করো যেগুলো handle করতে তুমি জানো, বাকিগুলো উপরে যেতে দাও যাতে real bug-গুলো দৃশ্যমান থাকে।

আরো দেখো

সম্পর্কিত পাঠ

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 দিয়ে।

আরও পড়ুন

Primitive Obsession: যখন সব কিছুই শুধু একটা string বা number

Primitive Obsession সহজ ভাষায় — কেন plain string আর number bug লুকিয়ে রাখে, আর কীভাবে Money বা Address-এর মতো value object দিয়ে code-কে safe আর পরিষ্কার করা যায়।

আরও পড়ুন

Long Method: যখন একটা function সব কিছু করতে চায়

Long Method code smell শিখো সহজ গল্পের মাধ্যমে — TypeScript আর C# example সহ, Extract Method দিয়ে step-by-step refactoring। একদম beginner-friendly গাইড।

আরও পড়ুন

Switch Statements: সেই রিসেপশনিস্ট আর তার বিশাল নিয়মের খাতা

Switch Statements code smell শেখো একটা school-এর গেটকিপারের গল্পের মাধ্যমে — TypeScript আর C#-এ duplicate switch-এর উদাহরণ সহ, আর কীভাবে polymorphism দিয়ে এটা ঠিক করবে।

আরও পড়ুন