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

এরর হ্যান্ডলিং — যেকোনো কিছু ভুল হতে পারে, প্রস্তুত থাকুন

Error Handling in C — Return Codes, errno, Cleanup & Defensive Programming

Taught in Bangla, this lesson covers error handling in C, which has no exceptions: return codes and errno, propagating errors to the caller, checking every result, goto cleanup, when to assert versus recover, and defensive programming at boundaries.

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

  • Programmer error আর runtime error-এর পার্থক্য — কোনটি কীভাবে সামলাতে হয়
  • Return code, errno, perror, strerror — C-তে ভুলের খবর পাঠানোর নিয়ম
  • strtol দিয়ে নিরাপদে সংখ্যা পড়া, আর goto cleanup দিয়ে leak ছাড়া বের হওয়া
  • কখন প্রোগ্রাম থামবে (fail fast) আর কখন কম সুবিধা নিয়ে চলতে থাকবে (graceful degradation)
  • AI-এর লেখা কোডের ভুল-পথগুলো যাচাইয়ের একটি চেকলিস্ট

১ · ভুল হবেই — প্রশ্ন হলো তখন প্রোগ্রাম কী করবে

সন্ধ্যা সাতটা, ঢাকার একটি ছোট দোকান। দোকানদার নিজের বানানো একটি হিসাবের প্রোগ্রামে দিনের বিক্রি তুলছেন। হঠাৎ লোডশেডিং। IPS চালু হলো, কম্পিউটার বেঁচে গেল। কিন্তু প্রোগ্রামটি সেই মুহূর্তে ফাইলে লিখছিল। বিদ্যুৎ ফিরলে দেখা গেল — হিসাবের ফাইল অর্ধেক লেখা, প্রোগ্রাম আর খুলছেই না।

যোগ-বিয়োগে কোনো ভুল ছিল না। সমস্যা হলো, প্রোগ্রামটি ধরে নিয়েছিল সবকিছু সবসময় ঠিকঠাক চলবে। বাস্তব পৃথিবী এমন নয়। ডিস্ক ভরে যায়, ফাইল কেউ মুছে ফেলে, নেটওয়ার্ক কেটে যায়, মেমরি ফুরিয়ে যায়, আর ব্যবহারকারী বয়সের ঘরে লেখেন “বাইশ”।

ভালো প্রোগ্রাম ভুল এড়ায় না —
ভুলের জন্য আগে থেকেই প্রস্তুত থাকে।

এই প্রস্তুতির নামই error handling. পেশাদার সফটওয়্যারে প্রায়ই দেখা যায়, আসল কাজের কোডের চেয়ে ভুল সামলানোর কোড বেশি। শুনতে অদ্ভুত লাগলেও এটাই স্বাভাবিক — কারণ “সব ঠিক থাকলে” পথ একটাই, কিন্তু “কিছু ভুল হলে” পথ অনেকগুলো।

দুই জাতের ভুল — এবং কেন এদের আলাদা করে চেনা জরুরি

সব ভুল এক রকম নয়। এই পর্বের সবচেয়ে গুরুত্বপূর্ণ ভাগটি এখানেই:

Programmer error

Programmer error মানে আপনার নিজের কোডের যুক্তিতে ভুল — যেমন নিজের কোড থেকেই array-তে ঋণাত্মক index পাঠানো, বা যে pointer কখনো NULL হওয়ার কথা নয় সেটি NULL. এটি একটি bug. সমাধান: কোড ঠিক করা।

ধরার যন্ত্র: assert, sanitizer, test

Runtime error

Runtime error মানে কোড ঠিক আছে, কিন্তু পরিবেশ সহযোগিতা করছে না — ফাইল নেই, ডিস্ক ভরা, মেমরি শেষ, ইনপুট ভুল। এগুলো ঘটবেই। সমাধান: প্রোগ্রামের ভেতরে সামলানোর ব্যবস্থা রাখা।

সামলানোর যন্ত্র: return code, errno, নতুন চেষ্টা

বাসের ড্রাইভার ভুল রাস্তায় বাঁক নিলে সেটা তাঁর ভুল — প্রশিক্ষণ দিয়ে ঠিক করতে হবে। কিন্তু হঠাৎ জ্যাম বা চাকা পাংচার ঘটবেই; এর জন্য বাসে থাকে অতিরিক্ত চাকা, আর ড্রাইভার জানেন বিকল্প রাস্তা। প্রথমটি bug, দ্বিতীয়টি runtime error.

সংক্ষেপে: bug ঠিক করতে হয়; runtime error সামলাতে হয়। অংশ ৬-এ এর জন্য দুটি আলাদা যন্ত্র দেখব।

২ · C-তে exception নেই — ভুলের খবর পাঠানোর উপায়

Java, Python বা C++-এ ভুল হলে একটি exception “ছোড়া” হয়, আর সেটি নিজে থেকেই ফাংশনের পর ফাংশন পেরিয়ে ওপরে উঠে যায়, যতক্ষণ না কেউ তাকে “ধরে”। C-তে এমন কোনো ব্যবস্থা নেই। C-র দর্শন সহজ: ফাংশন তার ফলাফলের সাথেই জানিয়ে দেবে কাজ হলো কি না, আর যে ডেকেছে সে যাচাই করবে।

Standard library আর বাস্তবের C কোডে মূলত চারটি উপায় দেখবেন:

উপায়কেমন দেখায়কোথায় দেখবেন
সংখ্যা ফেরত: সফল হলে 0, ব্যর্থ হলে -1if (close(fd) == -1) ...POSIX-এর অনেক ফাংশন
Pointer ফেরত: ব্যর্থ হলে NULLif (p == NULL) ...malloc, fopen, strchr
কারণ জানাতে বৈশ্বিক errnoperror("open")fopen, strtol, system call
ফলাফল output parameter-এ, ফেরত-মান শুধু অবস্থাif (parse(s, &n) != 0) ...যত্ন করে লেখা নিজের ফাংশন

Return code — সবচেয়ে পুরোনো ও সরল উপায়

ফেরত-মান দিয়ে সফলতা বা ব্যর্থতা জানানোকে বলে return code. আপনি এটি আগেই দেখেছেন — main-এর return 0 আসলে একটি return code, যা অপারেটিং সিস্টেম পায় exit status হিসেবে (পর্ব ০৪)। একই চিন্তা আপনার প্রতিটি ফাংশনে চালানো যায়।

সমস্যা একটাই: ফাংশন নিজেই সংখ্যা ফেরত দিলে (যেমন গড় নম্বর) -1 কি “ভুল” নাকি “গড় -1”? তাই পেশাদার কোডে প্রায়ই চতুর্থ উপায় — ফলাফল যায় output parameter-এ (পর্ব ১৮), আর ফেরত-মান শুধু বলে “কাজ হলো কি না”।

errno — “কেন ব্যর্থ হলো” তার খবর

fopen যখন NULL ফেরত দেয়, তখন শুধু জানা যায় “খোলা যায়নি”। কিন্তু কেন? ফাইল নেই? অনুমতি নেই? এই কারণটি জানাতে C-তে আছে errno — <errno.h>-এ ঘোষিত একটি পূর্ণসংখ্যা, যেখানে ব্যর্থ ফাংশন একটি কোড রেখে যায়। যেমন ENOENT মানে “এমন ফাইল বা ফোল্ডার নেই”, EACCES মানে “অনুমতি নেই”, ERANGE মানে “মান সীমার বাইরে”।

এই সংখ্যা মানুষের পড়ার মতো লেখায় বদলাতে দুটি ফাংশন আছে:

  • perror("app.conf") — আপনার দেওয়া লেখার পরে কোলন বসিয়ে কারণটি সরাসরি stderr-এ ছাপে।
  • strerror(errno) — কারণটি একটি string হিসেবে ফেরত দেয়, যাতে আপনি নিজের মতো করে বার্তা সাজাতে পারেন।
$ ./shop
app.conf: No such file or directory

errno-র তিনটি ফাঁদ। (১) সফল ফাংশন সাধারণত errno শূন্য করে না, পুরোনো কোড পড়ে থাকতে পারে — তাই আগে ফেরত-মান দেখে নিশ্চিত হন যে ভুল হয়েছে, তারপর errno পড়ুন। (২) মাঝে অন্য library ফাংশন (এমনকি printf) ডাকলে errno বদলাতে পারে — সাথে সাথে কপি করে রাখুন। (৩) কিছু ফাংশন (যেমন strtol) ব্যর্থ হলেও আলাদা কোনো ফেরত-মান দেয় না; সেখানে ডাকার আগে নিজে errno = 0 করে নিতে হয়।

মূল ধারণা

উপায় যেটাই বেছে নিন, পুরো প্রজেক্টে একটিই নিয়ম রাখুন এবং header ফাইলের মন্তব্যে লিখে দিন (পর্ব ২৯)। যেমন: “এই module-এর সব ফাংশন সফল হলে 0, ব্যর্থ হলে -1 ফেরত দেয় এবং errno বসায়।” এক ফাংশন 0-কে সফল আর আরেকটি 0-কে ব্যর্থ ধরলে সেই প্রজেক্টে ভুল হবেই — কারণ যে ডাকছে সে প্রতিবার মনে রাখতে পারবে না।

৩ · ভুল কীভাবে ওপরে ওঠে — আর কে সিদ্ধান্ত নেয়

গার্মেন্টস কারখানার সেলাই লাইনে একজন অপারেটরের সুতা ফুরিয়ে গেল। তিনি নিজে পুরো অর্ডার বাতিল করবেন না — সুপারভাইজারকে জানাবেন। অর্ডার কত জরুরি, গুদামে অন্য সুতা আছে কি না, সেটা সুপারভাইজার জানেন; তাই সিদ্ধান্তও তাঁর।

সফটওয়্যারেও ঠিক তাই। নিচের স্তরের ফাংশন জানে কী ভুল হয়েছে, কিন্তু জানে না সেটার মানে কী। open_file জানে ফাইলটি পাওয়া যায়নি; কিন্তু জানে না ফাইলটি জরুরি নাকি ঐচ্ছিক। এই কারণে ভুলের খবর ধাপে ধাপে ওপরে পাঠানো হয় — একে বলে error propagation. নিচের ডায়াগ্রামে একটি দোকানের প্রোগ্রাম খোলার সময় তার সেটিং ফাইল app.conf খুঁজছে, কিন্তু ফাইলটি নেই। “পরে” চাপুন।

1int open_file(const char *path, FILE **out) {2    FILE *f = fopen(path, "r");3    if (f == NULL) return -1;4    *out = f;  return 0;5}6int read_config(Config *cfg) {7    FILE *f;8    if (open_file("app.conf", &f) != 0) {9        perror("app.conf");10        return -1;11    }12    /* ... settings parse ... */13}14int main(void) {15    if (read_config(&cfg) != 0) {16        fprintf(stderr, "using defaults\n");17        use_defaults(&cfg);18    }19    run_shop(&cfg);
অবস্থা (Watch)
errno
f
open_file ফেরত
read_config ফেরত
সিদ্ধান্ত
  1. main কাজ দিল। সবচেয়ে ওপরের স্তর main বলল, “সেটিং পড়ে আনো।” এখনো কিছু ভুল হয়নি, errno শূন্য।
  2. দায়িত্ব নিচে নামল। read_config ফাইল খোলার কাজটি দিল আরও নিচের স্তর open_file-কে।
  3. আসল ভুলটি ঘটল। fopen ফাইলটি পেল না, তাই NULL ফেরত দিল আর errno-তে রেখে গেল ENOENT (Linux-এ যার মান 2) — অর্থাৎ “এমন ফাইল নেই”।
  4. খবর এক ধাপ ওপরে। open_file কিছু ছাপল না, প্রোগ্রামও বন্ধ করল না — শুধু -1 ফেরত দিল। কারণটি রয়ে গেল errno-তে।
  5. মাঝের স্তর কারণটি লিখে রাখল। read_config ফেরত-মান যাচাই করে ভুল দেখল, আর perror দিয়ে stderr-এ লিখল কোন ফাইল, কী সমস্যা।
  6. খবর আবার ওপরে। read_config-ও -1 ফেরত দিল। খবরটি পৌঁছাল সেই স্তরে, যে পুরো প্রোগ্রামের উদ্দেশ্য জানে।
  7. main সিদ্ধান্ত নিল। এই দোকানের প্রোগ্রামে সেটিং ফাইল না থাকলেও চলে, তাই main সতর্কবার্তা দিয়ে default মান নিল। অন্য প্রোগ্রামে সিদ্ধান্ত ভিন্ন হতে পারত: নেটওয়ার্কের ফাইল হলে কয়েক সেকেন্ড পরে retry, আর ফাইলটি অপরিহার্য হলে স্পষ্ট বার্তা দিয়ে return EXIT_FAILURE.
  8. প্রোগ্রাম চলতে থাকল। দোকান বন্ধ হলো না, আবার ভুলটি চাপাও পড়ল না — stderr-এ লেখা রইল কী ঘটেছে। ভালো error handling দেখতে এমনই।

খেয়াল করুন, তিনটি স্তরের তিনটি আলাদা দায়িত্ব: সবচেয়ে নিচের স্তর ভুল শনাক্ত করে, মাঝের স্তর প্রসঙ্গ যোগ করে, সবচেয়ে ওপরের স্তর সিদ্ধান্ত নেয়। তাই library ফাংশন নিজে exit() ডাকে না — সে জানে না তাকে কে, কোন কাজে ব্যবহার করছে।

৪ · প্রতিটি ফলাফল যাচাই করুন — চুপচাপ উপেক্ষার দাম

Error handling-এর প্রথম নিয়মটি শুনতে খুব সাধারণ: যে ফাংশন ব্যর্থ হতে পারে, তার ফেরত-মান যাচাই করুন। তবু বাস্তবের অসংখ্য বাগ ঠিক এখান থেকেই আসে। কোন কোন জায়গায় চোখ রাখবেন:

ফাংশনব্যর্থ হলে কী দেয়যাচাই না করলে কী হয়
fopenNULL + errnoNULL দিয়ে fgets ডাকলেই crash
malloc, reallocNULLNULL-এ লেখা — segmentation fault বা আরও খারাপ (পর্ব ২০)
scanfকতগুলো মান পড়া গেল তার সংখ্যা, বা EOFভ্যারিয়েবলে আবর্জনা মান থেকে যায়, হিসাব ভুল আসে
fgetsNULL (ফাইল শেষ বা ভুল)পুরোনো লেখাই আবার প্রসেস হয়
fclose, fprintfEOF বা ঋণাত্মক সংখ্যাডিস্ক ভরা থাকলেও প্রোগ্রাম বলে “সংরক্ষিত হয়েছে”

শেষ সারিটি খেয়াল করুন। buffer-এ জমে থাকা লেখা প্রায়ই ডিস্কে যায় fclose-এর সময় (পর্ব ১০-এর buffering মনে করুন), তাই ডিস্ক ভরা থাকলে ব্যর্থতা ধরা পড়ে ঠিক তখনই।

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

নিরাপদে সংখ্যা পড়া — strtol-এর তিনটি যাচাই

“ব্যবহারকারী সংখ্যার জায়গায় অক্ষর লিখলেন” — এটি সবচেয়ে পরিচিত runtime error. পর্ব ১০-এ দেখেছিলেন, পেশাদার পথ হলো আগে fgets দিয়ে পুরো লাইন পড়া, তারপর যাচাই করে সংখ্যায় বদলানো। এই বদলানোর কাজের সঠিক যন্ত্র strtol. atoi ব্যবহার করবেন না — atoi("abc") চুপচাপ 0 দেয়, ফলে “ভুল ইনপুট” আর “সত্যিকারের শূন্য” আলাদা করা যায় না।

strtol নিজে থেকে ভুল জানায় না; তাকে ঠিকভাবে প্রশ্ন করতে হয়। তিনটি যাচাই লাগে: কোনো অঙ্ক পড়া গেল কি না, শেষে বাড়তি কিছু রয়ে গেল কি না, আর সংখ্যাটি long-এর সীমা ছাড়াল কি না। নিচের কোডটি চালিয়ে দেখুন:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int parse_long(const char *s, long *out) {     /* 0 = OK, else error */
    char *end;  errno = 0;                     /* clear old errors first */
    long v = strtol(s, &end, 10);
    if (end == s || *end != '\0') return 1;   /* no digits, or extra junk */
    if (errno == ERANGE) return 2;             /* too big or too small */
    *out = v;
    return 0;
}

int main(void) {
    const char *in[] = {"42", "abc", "12x", "", "99999999999999999999"};
    const char *why[] = {"OK", "not a number", "out of range"};
    for (int i = 0; i < 5; i++) {
        long n = 0;  int rc = parse_long(in[i], &n);
        printf("\"%s\" -> %s (n = %ld)\n", in[i], why[rc], n);
    }
    return 0;
}

strtol দ্বিতীয় argument end-এ জানায় কোথায় পড়া থামল। end == s মানে একটি অঙ্কও পড়া যায়নি — "abc" আর খালি লেখা এখানে ধরা পড়ে। *end != '\0' মানে সংখ্যার পরে বাড়তি কিছু আছে — "12x"-কে আমরা 12 হিসেবে মেনে নিইনি। আর বিশ অঙ্কের সংখ্যাটি long-এ আঁটে না, তাই strtol errno-তে ERANGE বসায় (এজন্যই আগে errno = 0 করা জরুরি)। খেয়াল করুন, ভুলের ক্ষেত্রে n ছোঁয়াই হয়নি — 0-ই রয়ে গেছে। এটাই output parameter আর status আলাদা রাখার সুবিধা।

৫ · গুছিয়ে বের হওয়া — goto cleanup

ভুল ধরা হলো অর্ধেক কাজ। বাকি অর্ধেক হলো ভুলের পরে পরিষ্কারভাবে বের হওয়া। ধরুন একটি ফাংশন প্রথমে একটি ফাইল খুলল, তারপর একটি buffer-এর জন্য মেমরি নিল, তারপর আরেকটি টেবিলের জন্য মেমরি চাইল — আর তৃতীয়টি ব্যর্থ হলো। এখন শুধু return -1 লিখলে খোলা ফাইল আর প্রথম buffer চিরকালের জন্য আটকে থাকবে। এটাই memory leak আর resource leak (পর্ব ২০)।

সন্ধ্যায় অফিস থেকে বের হওয়ার সময় আপনি কী করেন? সকালে ঢুকে প্রথমে তালা খুলেছিলেন, তারপর লাইট জ্বালিয়েছিলেন, তারপর ফ্যান। বের হওয়ার সময় ঠিক উল্টো ক্রমে — ফ্যান বন্ধ, লাইট বন্ধ, শেষে তালা। মাঝপথে জরুরি ফোনে দৌড়ে বের হতে হলেও এই ক্রম মানলে কিছু চালু থেকে যায় না। রিসোর্স ছাড়ার নিয়মও এটাই: যা শেষে নিয়েছেন, তা আগে ছাড়ুন।

প্রতিটি if-এর ভেতরে আলাদা করে সব ছাড়ার কোড লিখলে কিছু একটা ছাড়তে ভুলে যাওয়া প্রায় নিশ্চিত। সমাধান হলো C-র বহুল প্রচলিত কৌশল goto cleanup. ফাংশনের শেষে cleanup-এর জন্য একটি জায়গা রাখা হয়, আর ভুল হলেই সেখানে লাফ দেওয়া হয়। নিচে Linux kernel-এর ধরনে লেখা একটি উদাহরণ দেখুন:

1int process(const char *path) {2    int rc = -1;3    char *buf = NULL, *tbl = NULL;4    FILE *f = fopen(path, "r");5    if (f == NULL) goto out;6    buf = malloc(4096);7    if (buf == NULL) goto close_file;8    tbl = malloc(100000 * sizeof(Row));9    if (tbl == NULL) goto free_buf;10    rc = do_work(f, buf, tbl);11    free(tbl);12free_buf:13    free(buf);14close_file:15    fclose(f);16out:17    return rc;18}
ভ্যারিয়েবল ও খোলা রিসোর্স
rc
f
buf
tbl
আটকে থাকা রিসোর্স
  1. হতাশাবাদী শুরু। rc শুরুতেই -1 — অর্থাৎ “প্রমাণ না হওয়া পর্যন্ত ধরে নিই ব্যর্থ”। সব pointer শুরুতে NULL. শেষে সবকিছু সফল হলেই কেবল rc বদলাবে।
  2. প্রথম রিসোর্স: ফাইল। fopen সফল — একটি রিসোর্স হাতে। ব্যর্থ হলে সরাসরি out-এ যেতাম, কারণ তখনো ছাড়ার মতো কিছু নেই।
  3. দ্বিতীয় রিসোর্স: buffer. ৪০৯৬ বাইট পাওয়া গেল, হাতে দুটি রিসোর্স। এখানে ব্যর্থ হলে লাফ দিতাম close_file-এ।
  4. তৃতীয় রিসোর্স ব্যর্থ! বড় টেবিলের জন্য মেমরি পাওয়া গেল না, malloc দিল NULL. এখন সাবধান: আগের দুটি রিসোর্স এখনো আমাদের হাতে।
  5. লাফ: goto free_buf. আসল কাজ (লাইন ১০–১১) বাদ। আমরা গেলাম সেই label-এ, যেখান থেকে শুধু যা নেওয়া হয়েছিল তা ছাড়া শুরু হয়।
  6. উল্টো ক্রমে প্রথম: buffer ছাড়া। শেষে নেওয়া buffer আগে ছাড়া হলো। এরপর কোনো return নেই — কোড নিচের লাইনে “গড়িয়ে” যায়।
  7. তারপর ফাইল বন্ধ। সবার আগে নেওয়া ফাইলটি সবার শেষে বন্ধ হলো। অফিসের তালার মতো।
  8. একটিমাত্র বের হওয়ার দরজা। ফাংশন -1 ফেরত দিল, একটি রিসোর্সও আটকে নেই। সফল হলেও একই পথে বের হতো — লাইন ১১-তে tbl ছেড়ে নিচে গড়িয়ে। সব পথ এক জায়গায় মেলে বলে কোনো পথে ছাড়তে ভুলে যাওয়ার সুযোগ নেই।

বড় প্রজেক্টে label-গুলো এভাবে সিঁড়ির মতো সাজানো থাকে। ছোট ফাংশনে আরও সরল একটি রূপ চলে: একটিমাত্র cleanup: label, আর সেখানে সব pointer ছাড়া — কারণ free(NULL) নিরাপদ, কিছুই করে না। নিচের কোডটি একটি কৃত্রিম ব্যর্থতা তৈরি করে দেখায় যে কোন ধাপেই ব্যর্থ হোক, ঠিক ততটুকুই ছাড়া হয় যতটুকু নেওয়া হয়েছিল:

#include <stdio.h>
#include <stdlib.h>

int load_report(int fail_at) {         /* fail_at: which step pretends to fail */
    int rc = -1;  char *file = NULL, *buf = NULL, *table = NULL;
    if ((file  = fail_at == 1 ? NULL : malloc(64)) == NULL) goto cleanup;
    if ((buf   = fail_at == 2 ? NULL : malloc(64)) == NULL) goto cleanup;
    if ((table = fail_at == 3 ? NULL : malloc(64)) == NULL) goto cleanup;
    printf("  all 3 resources ready, work done\n");
    rc = 0;
cleanup:                               /* reverse order; free(NULL) is safe */
    printf("  cleanup: table=%s buf=%s file=%s\n",
           table ? "freed" : "-", buf ? "freed" : "-", file ? "freed" : "-");
    free(table); free(buf); free(file);
    return rc;
}

int main(void) {
    for (int step = 0; step <= 3; step++)
        printf("fail_at=%d -> rc=%d\n", step, load_report(step));
    return 0;
}

fail_at দিয়ে আমরা ইচ্ছে করে কোনো একটি ধাপ ব্যর্থ করাচ্ছি — যাতে ভুলের পথগুলোও চোখে দেখা যায়। fail_at=0-তে সব সফল, তিনটিই ছাড়া হয়। fail_at=1-এ কিছুই নেওয়া হয়নি, তাই কিছুই ছাড়া হয় না (-)। fail_at=3-এ প্রথম দুটি ছাড়া হয়। সব পথ একটি cleanup-এ মিলেছে বলে কোনো পথেই leak নেই। বাস্তবে file হবে fopen-এর FILE *. তখন সাবধান: fclose(NULL) নিরাপদ নয়, তাই লিখতে হবে if (file) fclose(file);

১৯৬৮ সালে Edsger Dijkstra তাঁর বিখ্যাত চিঠি “Go To Statement Considered Harmful”-এ দেখান যে যত্রতত্র goto কোডকে জট পাকানো সুতার মতো করে তোলে (পর্ব ১২-এর structured programming মনে করুন)। তবু Linux kernel-এর coding style নির্দেশিকা ফাংশনের শেষে একত্রে বের হওয়ার জন্য goto-কে স্পষ্টভাবে সমর্থন করে। পার্থক্যটি দিকের: এখানে লাফ সবসময় নিচের দিকে, একটিমাত্র উদ্দেশ্যে — পরিষ্কার করে বের হওয়া। ওপরে লাফ নেই, লুপ বানানো নেই। C-তে স্বয়ংক্রিয় পরিষ্কারের ব্যবস্থা (যেমন Go-র defer) নেই বলেই এই শৃঙ্খলাবদ্ধ goto আজও টিকে আছে।

সাবধান — “goto fail”. ২০১৪ সালে Apple-এর iOS ও macOS-এর SSL/TLS কোডে একটি বিখ্যাত বাগ ধরা পড়ে। ব্রেস ছাড়া একটি if-এর নিচে goto fail; ভুল করে দুবার লেখা ছিল। দ্বিতীয়টি সবসময় চলত, ফলে নিরাপত্তা-সনদের একটি জরুরি যাচাই বাদ পড়ত — অথচ ফেরত-মান বলত “সব ঠিক”। শিক্ষা: if-এ সবসময় { } দিন, -Wall চালু রাখুন, আর rc-এর শুরুর মান রাখুন “ব্যর্থ”।

৬ · assert নাকি সামলানো? থামবেন, নাকি চলবেন?

assert — নিজের কোডের প্রতি সন্দেহ

<assert.h>-এর assert একটি macro (পর্ব ২৭)। আপনি লেখেন assert(i >= 0 && i < n); — অর্থাৎ “আমার কোড ঠিক থাকলে এটা সবসময় সত্য হবে”। শর্তটি মিথ্যা হলে প্রোগ্রাম সাথে সাথে থেমে যায় এবং ফাইলের নাম, লাইন নম্বর আর শর্তটি ছাপে:

shop: stock.c:42: get_item: Assertion `i >= 0 && i < n' failed.
Aborted (core dumped)

কিন্তু একটি বড় শর্ত আছে। NDEBUG নামের ম্যাক্রো define করে compile করলে (যেমন gcc -DNDEBUG) সব assert পুরোপুরি মুছে যায় — কোডে তাদের কোনো অস্তিত্বই থাকে না। অনেক প্রজেক্ট release build-এ ঠিক এটাই করে। এর দুটি পরিণাম:

  • assert দিয়ে কখনো runtime error সামলাবেন না। assert(fopen(...) != NULL) লিখলে release build-এ যাচাইটি হারিয়ে যাবে।
  • assert-এর ভেতরে কোনো কাজ রাখবেন না। assert(init_db() == 0) লিখলে release build-এ init_db() ডাকাই হবে না!

Compile-এর সময়েই যা যাচাই করা যায় (যেমন “int অন্তত ৪ বাইট”), তার জন্য আছে static_assert (C11 থেকে) — শর্ত মিথ্যা হলে প্রোগ্রাম তৈরিই হয় না, তাই NDEBUG-এর প্রশ্নও আসে না।

Fail fast বনাম graceful degradation

Runtime error হলে প্রোগ্রামের সামনে দুটি পথ। Fail fast মানে “স্পষ্ট বার্তা দিয়ে এখনই থামো, যাতে ভুল ছড়িয়ে না পড়ে”। Graceful degradation মানে “যা কাজ করছে না তা বাদ দিয়ে, কম সুবিধা নিয়ে হলেও চলতে থাকো”। লোডশেডিংয়ে IPS যেমন এসি চালায় না, কিন্তু লাইট-ফ্যান চালু রাখে — পুরো বাড়ি অন্ধকার হয় না।

কোন পরিস্থিতিতে কোন পথ — তা ঠিক করতে নিচের সিদ্ধান্ত-ছকটি চারটি বাস্তব পরিস্থিতিতে প্রয়োগ করে দেখুন:

প্রশ্ন ১: আমার কোডের ভুল?কোড ঠিক থাকলে এটা কখনো ঘটত না
প্রশ্ন ১: পরিবেশ বা ইনপুটের সমস্যা?ফাইল, মেমরি, নেটওয়ার্ক, ব্যবহারকারী
প্রশ্ন ২: এটা ছাড়া কি অর্থবহভাবে চলা যায়?নিরাপদে, ভুল ফল না দিয়ে
assertধরা পড়লেই থামো, কোড ঠিক করো
হ্যাঁ → সামলাওdefault মান, আবার চেষ্টা, নতুন ইনপুট
না → fail fastস্পষ্ট বার্তা + exit status 1
পরিস্থিতি ১বড় ফাইল প্রসেস করতে malloc ব্যর্থ
char *data = malloc(size);
if (data == NULL) {
    fprintf(stderr, "convert: not enough memory for %zu bytes\n", size);
    return EXIT_FAILURE;     /* ছোট টুল: থামাই নিরাপদ */
}
পরিস্থিতি ২নিজের কোড থেকে ঋণাত্মক index
static int stock_at(const int *stock, int n, int i) {
    assert(i >= 0 && i < n);   /* ডাকছে আমার নিজের কোডই */
    return stock[i];
}
পরিস্থিতি ৩ঐচ্ছিক সেটিং ফাইল নেই
if (read_config(&cfg) != 0) {
    fprintf(stderr, "warning: app.conf missing, using defaults\n");
    use_defaults(&cfg);     /* কম সুবিধায় চলতে থাকো */
}
পরিস্থিতি ৪বয়সের ঘরে ব্যবহারকারী লিখলেন "abc"
while (parse_long(line, &age) != 0 || age < 0 || age > 150) {
    printf("Please enter a number between 0 and 150: ");
    if (fgets(line, sizeof line, stdin) == NULL) return 1;
}
  1. দুটি প্রশ্নের ছক। প্রথম প্রশ্ন: ভুলটি কার — আমার কোডের, নাকি বাইরের পৃথিবীর? কোডের হলে উত্তর assert. বাইরের হলে দ্বিতীয় প্রশ্ন: এটা ছাড়া কি নিরাপদে ও সঠিকভাবে চলা যায়? হ্যাঁ হলে সামলাও, না হলে স্পষ্ট বার্তা দিয়ে থামো।
  2. malloc ব্যর্থ। মেমরি ফুরানো পরিবেশের সমস্যা, তাই assert নয়। ছোট ফাইল-রূপান্তরের টুলে মেমরি ছাড়া কাজটাই অসম্ভব, তাই fail fast: কোন কাজ, কত বাইট লিখে EXIT_FAILURE. দীর্ঘ সময় চলা সার্ভারে অবশ্য শুধু সেই অনুরোধটি বাতিল করে বাকিদের সেবা চালু রাখা হয়।
  3. নিজের কোড থেকে ঋণাত্মক index. stock_at-কে ডাকে শুধু আপনার নিজের কোড। সেখান থেকে -1 এলে বুঝতে হবে কোথাও হিসাবে ভুল — এটা bug. ব্যবহারকারীকে বার্তা দিয়ে লাভ নেই; দরকার ভুলটা যত দ্রুত সম্ভব ডেভেলপারের চোখে পড়া। তাই assert.
  4. ঐচ্ছিক সেটিং ফাইল নেই। পরিবেশের সমস্যা, কিন্তু default মান দিয়ে দিব্যি চলা যায় — তাই graceful degradation, তবে stderr-এ সতর্কবার্তা রেখে। ফাইলটি অপরিহার্য হলে উত্তর হতো fail fast.
  5. ব্যবহারকারী লিখলেন “abc” — মানুষ টাইপে ভুল করবেনই — এটা সবচেয়ে প্রত্যাশিত runtime error. থামার কোনো কারণ নেই; স্পষ্ট করে বলুন কী চাই, আবার জিজ্ঞেস করুন। শুধু ইনপুট শেষ হয়ে গেলে (fgets দিল NULL) থামুন, নইলে লুপ কখনো শেষ হবে না।
মূল ধারণা

Assert আপনার জন্য, error handling ব্যবহারকারীর জন্য। assert বলে “ডেভেলপার, আপনার কোডে ভুল আছে”; error handling বলে “বাইরে সমস্যা হয়েছে, প্রোগ্রাম এভাবে সামলাচ্ছে”। আর থামবেন নাকি চলবেন, সেই প্রশ্নের নিয়ম: ভুল ফল দেওয়ার চেয়ে থেমে যাওয়া ভালো; কিন্তু নিরাপদে চলা গেলে অকারণে থামা খারাপ।

৭ · Defensive programming — সীমানায় পাহারা

Defensive programming মানে এমনভাবে কোড লেখা, যেন বাইরের প্রতিটি তথ্যকে সন্দেহের চোখে দেখা হয়। তবে প্রতিটি লাইনে সব কিছু যাচাই করা নয় — তাতে কোড পড়াই কঠিন হয়ে যায়। পেশাদার নিয়মটি হলো সীমানায় পাহারা।

মেট্রো রেলে ঢোকার সময় গেটে একবার কার্ড যাচাই হয়; ট্রেনের প্রতিটি বগিতে আবার হয় না, কারণ ভেতরের সবাই গেট পেরিয়েই এসেছেন। প্রোগ্রামেও তাই: ফাইল, কীবোর্ড, নেটওয়ার্ক বা অন্য প্রোগ্রাম থেকে তথ্য যেখানে প্রথম ঢোকে, সেখানে কড়া যাচাই — একে বলে input validation বা ইনপুট যাচাই। ভেতরের নিজস্ব ফাংশনগুলোর মধ্যে চুক্তি পাহারা দেয় assert.

Overflow ঘটার আগেই ধরা

পর্ব ০৫-এ দেখেছিলেন, int-এর সীমা ছাড়ালে কী সর্বনাশ হতে পারে — Ariane 5 রকেটের গল্পটিও সেখানে আছে। আর signed int-এর overflow C-তে undefined behavior, তাই “আগে যোগ করি, পরে দেখি ফল উল্টে গেল কি না” পদ্ধতি কাজ করে না — compiler সেই যাচাইটাই মুছে দিতে পারে। নিয়ম হলো: যোগ করার আগেই হিসাব করুন যোগফল আঁটবে কি না। একটি মোবাইল ওয়ালেটের ব্যালান্সে টাকা যোগের উদাহরণে দেখুন:

1#include <limits.h>   /* INT_MAX = 2147483647 */2int add_balance(int bal, int amount, int *out) {3    if (amount > 0 && bal > INT_MAX - amount)4        return -1;          /* would overflow */5    if (amount < 0 && bal < INT_MIN - amount)6        return -1;          /* would underflow */7    *out = bal + amount;8    return 0;9}
মান
bal
amount
INT_MAX - amount
সরাসরি যোগ করলে
ফেরত
  1. প্রায় পূর্ণ ব্যালান্স। ব্যালান্স int-এর সর্বোচ্চ মানের খুব কাছে, তার সাথে ১০০০ যোগ করতে হবে। (বাস্তবে টাকা রাখা হয় পয়সার হিসাবে, int64_t-এ — কিন্তু সীমা সব টাইপেরই আছে।)
  2. যোগ না করে বিয়োগ। bal + amount > INT_MAX লিখলে যোগটাই আগে overflow হবে। তাই সমীকরণটি ঘুরিয়ে লেখা হয়: bal > INT_MAX - amount. amount ধনাত্মক বলে এই বিয়োগ কখনো সীমা ছাড়ায় না। ফল 2147482647.
  3. যাচাই ধরে ফেলল। 2147483000 > 2147482647 — সত্য। অর্থাৎ যোগ করলে সীমা ছাড়াত। সরাসরি যোগ করলে সাধারণত দেখা যেত একটি বিশাল ঋণাত্মক সংখ্যা — ধনী গ্রাহক মুহূর্তে দেনাদার! (আর যেহেতু এটা undefined behavior, আরও অদ্ভুত কিছুও ঘটতে পারত।)
  4. ভুল জানানো হলো, ক্ষতি নয়। ফাংশন -1 ফেরত দিল, *out ছোঁয়াই হলো না। ডাকছে যে কোড, সে লেনদেনটি বাতিল করে গ্রাহককে স্পষ্ট বার্তা দেবে। ব্যালান্স যেমন ছিল তেমনই আছে।

এই হিসাব হাতে লেখা ঝামেলার, আর ভুলও হয়। তাই GCC ও Clang দেয় __builtin_add_overflow(a, b, &result) — যোগফল আঁটলে result-এ রাখে আর false দেয়, না আঁটলে true. আর C23 মানে এসেছে <stdckdint.h>-এর ckd_add(&result, a, b) — একই কাজ, এবার মানসম্মতভাবে। নতুন কোডে এগুলোই ব্যবহার করুন।

ভালো error message — কাকে, কী, কোথায়

ভুলের বার্তা লেখা হয় দুজন মানুষের জন্য: যিনি প্রোগ্রাম ব্যবহার করছেন, আর পরে যিনি সমস্যা খুঁজবেন। নিচের তুলনাটি দেখুন:

খারাপভালোকেন ভালো
Error!shop: cannot open 'sales.csv': No such file or directoryকোন প্রোগ্রাম, কোন ফাইল, কী কারণ — তিনটিই আছে
Invalid inputage must be a number between 0 and 150 (got "abc")কী চাই আর কী পেলাম — দুটোই বলে
Error code 54dose setting does not match machine state; treatment stoppedসংখ্যা নয়, অর্থ বলে; পরিণামও বলে

তিনটি অভ্যাস রাখুন। এক, ভুলের বার্তা যাবে stderr-এ (পর্ব ১০), যাতে আউটপুট ফাইলে পাঠালেও বার্তাটি স্ক্রিনে থাকে। দুই, ব্যর্থ হলে শূন্য নয় এমন exit status (EXIT_FAILURE), যাতে script বা CI বোঝে কাজটি হয়নি। তিন, দীর্ঘ সময় চলা প্রোগ্রামে logging — ঘটনা ও ভুল সময়সহ ফাইলে লিখে রাখা, DEBUG, INFO, WARN, ERROR মাত্রায় ভাগ করে। রাত তিনটায় সার্ভার বিকল হলে সকালে হাতে থাকবে শুধু এই লগ।

৮ · বাস্তবের শিক্ষা — যখন ভুল সামলানো হয়নি

Error handling শুধু বইয়ের তত্ত্ব নয়। ইতিহাসে এমন দুটি ঘটনা দেখুন, যা আজও সফটওয়্যার প্রকৌশলের পাঠ্যক্রমে পড়ানো হয়।

Therac-25 (১৯৮৫–১৯৮৭): ক্যানসারের চিকিৎসায় ব্যবহৃত এই বিকিরণ যন্ত্রে অন্তত ছয়টি দুর্ঘটনায় রোগীরা নির্ধারিত মাত্রার বহুগুণ বিকিরণ পান; কয়েকজন মারা যান। পরে Nancy Leveson ও Clark Turner-এর বিখ্যাত তদন্তে কয়েকটি কারণ উঠে আসে। আগের মডেলে hardware-এর নিরাপত্তা-তালা (interlock) ছিল; এই মডেলে সেই দায়িত্ব পুরোপুরি সফটওয়্যারকে দেওয়া হয়। অপারেটর খুব দ্রুত চিকিৎসার তথ্য সংশোধন করলে একটি race condition-এর কারণে যন্ত্রের আসল অবস্থা আর পর্দার তথ্য মিলত না। আরেকটি ক্ষেত্রে একটি এক-বাইটের counter overflow হয়ে শূন্যে ফিরলে একটি নিরাপত্তা-যাচাই বাদ পড়ে যেত। আর ভুল হলে পর্দায় আসত শুধু “MALFUNCTION 54”-এর মতো অর্থহীন বার্তা — যা এত ঘনঘন আসত যে অপারেটররা একটি বোতাম চেপে চিকিৎসা চালিয়ে যাওয়াতে অভ্যস্ত হয়ে গিয়েছিলেন।

এই পর্বের প্রায় সব শিক্ষাই এখানে: overflow আগে যাচাই না করা, অর্থহীন error message, আর জীবন-মরণের প্রশ্নে fail safe না হওয়া — অবস্থা অস্পষ্ট হলেই বিকিরণ বন্ধ রাখা উচিত ছিল।

Knight Capital (১ আগস্ট ২০১২): যুক্তরাষ্ট্রের এই বড় শেয়ার-লেনদেন কোম্পানি নতুন একটি সফটওয়্যার আটটি সার্ভারে বসায়, কিন্তু একটি সার্ভারে নতুন কোড পৌঁছায়নি। নতুন কোডে একটি পুরোনো flag-কে নতুন অর্থে ব্যবহার করা হয়েছিল। ফলে সেই একটি সার্ভারে flag-টি জাগিয়ে তুলল বহু বছর আগে বাতিল হওয়া পুরোনো কোড, যা একের পর এক অবাঞ্ছিত লেনদেন করতে থাকল। মোটামুটি ৪৫ মিনিটে কোম্পানির ক্ষতি দাঁড়ায় প্রায় ৪৪ কোটি মার্কিন ডলার। টিকে থাকতে কোম্পানিটিকে জরুরি বাইরের বিনিয়োগ নিতে হয়।

এখানে শিক্ষা কিছুটা ভিন্ন। কোনো একটি ফাংশনের ফেরত-মান উপেক্ষার চেয়ে বড় ব্যাপার হলো সিস্টেম-স্তরের অপ্রস্তুতি: সব সার্ভারে একই কোড পৌঁছেছে কি না সেই যাচাই ছিল না, পুরোনো অব্যবহৃত কোড মুছে ফেলা হয়নি, আর ভুল ধরা পড়লে দ্রুত সব থামানোর (fail fast-এর) স্পষ্ট ব্যবস্থা ছিল না। Error handling তাই শুধু একটি if নয় — এটি পুরো সিস্টেম নকশার অংশ।

৯ · AI-এর যুগে এরর হ্যান্ডলিং

AI-কে “একটি ফাইল পড়ে গড় বের করার C প্রোগ্রাম লেখো” বললে সাধারণত যে কোড আসে, তা সব ঠিক থাকলে চমৎকার চলে। কিন্তু AI শেখে মূলত উদাহরণ-কোড থেকে, যেখানে জায়গা বাঁচাতে error handling প্রায়ই বাদ থাকে। তাই ভুলের পথগুলোই সবচেয়ে দুর্বল — আর সাধারণ পরীক্ষায় তা ধরা পড়ে না, কারণ তখন ফাইলও থাকে, মেমরিও থাকে।

AI সাধারণত যেসব ভুল করে

AI-এর ভুলকোডে যেভাবে দেখবেনকী হওয়া উচিত
ফেরত-মান উপেক্ষাFILE *f = fopen(...); তারপর সরাসরি ব্যবহারপ্রতিটি fopen, malloc, scanf-এর পরে যাচাই
Library-র গভীরে exit()সাহায্যকারী ফাংশনের ভেতরে exit(1)ফাংশন ভুল ফেরত দেবে; থামার সিদ্ধান্ত main-এর
ভুল গিলে ফেলাভুল হলে চুপচাপ return 0;ভুল হলে ভুলই জানানো
অস্পষ্ট বার্তাprintf("Error\n");stderr-এ কোন কাজ, কোন ফাইল, strerror(errno)
ভুলের পথে leakদ্বিতীয় malloc ব্যর্থ হলে প্রথমটি না ছেড়েই returnএক জায়গায় cleanup, উল্টো ক্রমে

AI-কে শুধু “কোড লেখো” না বলে ভুলের পথগুলো আলাদা করে চেয়ে নিন — এবং প্রতিটি ভুলের জন্য পরীক্ষাও। নিচের কাঠামোটি কাজে লাগাতে পারেন:

আমার জন্য একটি C ফাংশন লেখো: int load_prices(const char *path, double **out, size_t *count). এটি একটি টেক্সট ফাইল থেকে প্রতি লাইনে একটি দাম পড়বে। নিয়ম: ১. সফল হলে 0, ব্যর্থ হলে -1 ফেরত দেবে; ফাংশনের ভেতরে কোথাও exit() বা printf থাকবে না। ২. fopen, malloc/realloc, fclose — প্রতিটির ফেরত-মান যাচাই করবে। ৩. সংখ্যা পড়তে atoi/atof নয়, strtod ব্যবহার করবে, end pointer আর errno দুটোই যাচাই করবে। ৪. মাঝপথে ব্যর্থ হলে একটি cleanup label-এ গিয়ে যা নেওয়া হয়েছে সব উল্টো ক্রমে ছাড়বে। ৫. কোডের পরে একটি তালিকা দাও: কোন কোন ভাবে ফাংশনটি ব্যর্থ হতে পারে, আর প্রতিটির জন্য একটি করে পরীক্ষা (ফাইল নেই, খালি ফাইল, "12.5abc" লাইন, খুব বড় সংখ্যা, মেমরি না পাওয়া)।

AI-এর লেখা কোড হাতে পেলে — ভুলের পথের চেকলিস্ট

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Exception নাকি error code — কোনটি ভালো? আর Go ও Rust-এর মতো আধুনিক ভাষা কেন আবার স্পষ্ট error-এর পথে ফিরল?

দুটি পদ্ধতিরই নিজস্ব দাম আছে।

  • Exception-এর সুবিধা: মূল কোড পরিষ্কার থাকে — প্রতিটি লাইনের পরে if লাগে না। ভুল নিজে থেকেই ওপরে উঠে যায়, তাই কেউ “ভুলে উপেক্ষা” করতে পারে না; কেউ না ধরলে প্রোগ্রাম থেমে যায়।
  • Exception-এর দাম: কোড পড়ে বোঝা যায় না কোন লাইন থেকে exception বেরোতে পারে — নিয়ন্ত্রণ যেন অদৃশ্য পথে লাফায়। ফলে মাঝপথে বের হয়ে গেলে রিসোর্স ছাড়া হলো কি না, ডেটা আধা-বদলানো অবস্থায় রইল কি না — এসব নিশ্চিত করা কঠিন। C++-এ এজন্য RAII-এর মতো আলাদা শৃঙ্খলা লাগে।
  • Error code-এর সুবিধা: সব কিছু চোখের সামনে। কোন লাইন ব্যর্থ হতে পারে আর কী হবে, পড়লেই বোঝা যায়। কোনো লুকানো খরচ নেই — এজন্যই kernel ও embedded জগতে C-র পদ্ধতি টিকে আছে।
  • Error code-এর দাম: উপেক্ষা করা খুব সহজ — ফেরত-মান না দেখলে compiler কিছু বলে না (GCC/Clang-এর warn_unused_result attribute বা C23-এর [[nodiscard]] কিছুটা সাহায্য করে)। আর কোড লম্বা হয়।

আধুনিক সমাধান: Go (২০০৯) ইচ্ছাকৃতভাবে সাধারণ exception রাখেনি; ফাংশন ফলাফলের সাথে একটি error মান ফেরত দেয়, আর কোডে বারবার দেখা যায় if err != nil. Rust আরও এক ধাপ এগিয়েছে: Result<T, E> টাইপ — হয় ফলাফল, নয় ভুল। ফলাফল বের করতে হলে ভুলের সম্ভাবনা সামলাতেই হবে, নইলে compile হবে না; আর ? অপারেটর এক অক্ষরে ভুলটি ওপরে পাঠিয়ে দেয়। অর্থাৎ error code-এর স্পষ্টতা আর exception-এর “উপেক্ষা করা যায় না” গুণ — দুটো একসাথে। আর সত্যিকারের bug-এর জন্য Rust-এ আছে panic — ঠিক যেমন আমরা assert-কে আলাদা রেখেছি।

মূল উপলব্ধি: ভালো নকশা ভুলকে টাইপ-ব্যবস্থার অংশ বানায়, যাতে “ভুলে যাওয়া” অসম্ভব হয়। C-তে সেই সুবিধা নেই — তাই সেখানে শৃঙ্খলা আর code review-ই আপনার Result type.

প্র ০২errno কেন একটি বৈশ্বিক ভ্যারিয়েবল? এই নকশা থেকে কী কী সমস্যা হয়েছে?

১৯৭০-এর দশকের Unix-এ নকশাটি ছিল সরল ও সস্তা। একটি system call-এর ফেরত-মান একটাই সংখ্যা; সেখানে ফলাফল আর ভুলের কারণ দুটো রাখা কঠিন। তাই ফেরত-মান শুধু বলত “ব্যর্থ” (-1), আর কারণটি রাখা হতো একটি সাধারণ জায়গায় — errno. তখনকার প্রোগ্রাম একটি সুতোয় (single thread) চলত, তাই কোনো সমস্যা হয়নি।

সমস্যাগুলো এলো পরে:

  • Thread: একই প্রোগ্রামে কয়েকটি thread একসাথে চললে (পর্ব ৪৯) একজনের ভুলের কোড আরেকজন পড়ে ফেলতে পারত। সমাধান: আধুনিক সিস্টেমে, আর C11 মান অনুযায়ী, errno প্রতিটি thread-এর নিজস্ব — একে বলে thread-local ভ্যারিয়েবল। তাই errno আসলে প্রায়ই একটি ম্যাক্রো, যা বর্তমান thread-এর নিজস্ব ঘরকে দেখায়।
  • হারিয়ে যাওয়া কারণ: ভুলের পরে log লিখতে গিয়ে আরেকটি ফাংশন ডাকলেন — সেটি errno বদলে দিল। আসল কারণ মুছে গেল। তাই নিয়ম: সাথে সাথে কপি করুন।
  • Signal handler: প্রোগ্রামের মাঝখানে হঠাৎ চলা handler errno বদলালে মূল কোড বিভ্রান্ত হয়; তাই handler-এ শুরুতে errno সংরক্ষণ আর শেষে ফিরিয়ে দেওয়ার রীতি আছে।
  • অস্পষ্ট নিয়ম: কোন ফাংশন errno বসায়, কোনটি বসায় না, আর সফল হলে কী থাকে — ফাংশন ভেদে আলাদা। strtol-এর জন্য আগে শূন্য করতে হয়, fopen-এর জন্য লাগে না। মুখস্থ রাখতে হয় — ভুলের উৎস।

এজন্যই নতুন API-গুলো ভুলের কোড সরাসরি ফেরত দেয়। যেমন POSIX thread-এর ফাংশনগুলো (pthread_create ইত্যাদি) errno বসায় না, ভুলের কোডটাই ফেরত-মান হিসেবে দেয়।

মূল উপলব্ধি: বৈশ্বিক অবস্থা (global state) এক সময়ের সহজ সমাধান, পরে সমান্তরাল জগতে বোঝা হয়ে দাঁড়ায়। নিজের API নকশায় ভুলের তথ্য যতটা সম্ভব ফাংশনের ফেরত-মান বা আলাদা output parameter দিয়ে দিন।

প্র ০৩Therac-25 থেকে জীবন-নির্ভর (safety-critical) সফটওয়্যারের জন্য কী কী শিক্ষা নেওয়া হয়েছে?

Therac-25-কে প্রায়ই বলা হয় সফটওয়্যার নিরাপত্তার “পাঠ্যপুস্তকের ঘটনা”। Nancy Leveson-এর বিশ্লেষণের মূল কথা ছিল: এটা একটি বাগের গল্প নয়, একটি পুরো প্রক্রিয়ার ব্যর্থতা। কয়েকটি শিক্ষা:

  • একস্তরের সুরক্ষায় ভরসা নয়: আগের মডেলে hardware interlock ছিল; সফটওয়্যারে ভুল থাকলেও যন্ত্র বিপজ্জনক অবস্থায় যেতে পারত না। সেটি সরিয়ে পুরো ভার সফটওয়্যারে দেওয়া হয়। আজকের নীতি “defense in depth” — একাধিক স্বাধীন স্তর, যাতে একটি ব্যর্থ হলেও অন্যটি রক্ষা করে।
  • পুরোনো কোডের পুনর্ব্যবহারে সাবধান: আগের মডেলের কোড নতুন যন্ত্রে নেওয়া হয়েছিল। আগে যে বাগ hardware ঢেকে রাখত, এখন তা প্রকাশ পেল। Ariane 5-এর গল্পেও একই শিক্ষা — পরিবেশ বদলালে পুরোনো অনুমান মিথ্যা হয়।
  • Concurrency-র ভুল পরীক্ষায় সহজে ধরা পড়ে না: race condition নির্ভর করে সময়ের ওপর — শুধু দক্ষ অপারেটর দ্রুত টাইপ করলেই ঘটত। এজন্য আজ এমন সিস্টেমে formal verification আর কঠোর নকশা-পর্যালোচনা ব্যবহার হয়।
  • Error message মানুষের সিদ্ধান্তের অংশ: অর্থহীন আর ঘনঘন বার্তা অপারেটরদের শিখিয়ে দিয়েছিল সেগুলো উপেক্ষা করতে। বার্তা কম, স্পষ্ট ও কাজের হতে হবে।
  • Fail safe: অনিশ্চিত অবস্থায় যন্ত্রকে সবচেয়ে নিরাপদ অবস্থায় যেতে হবে — এখানে বিকিরণ বন্ধ।
  • অভিযোগকে গুরুত্ব দেওয়া: প্রথম দিকের দুর্ঘটনার রিপোর্ট পাওয়ার পরও প্রস্তুতকারক দীর্ঘদিন সফটওয়্যারকে সন্দেহ করেনি।

আজ চিকিৎসা-যন্ত্র, বিমান, রেল ও গাড়ির সফটওয়্যারের জন্য আলাদা মানদণ্ড আছে, যেখানে প্রতিটি ঝুঁকি চিহ্নিত করা, প্রতিটি ভুলের পথ পরীক্ষা করা আর স্বাধীনভাবে যাচাই করানো বাধ্যতামূলক। গাড়ির embedded C-এর জন্য MISRA C-এর মতো কঠোর নিয়মাবলিও এই চিন্তা থেকে এসেছে।

মূল উপলব্ধি: নিরাপত্তা কোনো একটি if-এর গুণ নয়, পুরো সিস্টেমের গুণ — hardware, software, মানুষ আর প্রক্রিয়া মিলে। কোড যত গুরুত্বপূর্ণ, ভুলের পথ নিয়ে ভাবনা তত গভীর হতে হবে।

প্র ০৪সবকিছু যাচাই করতে গেলে কোড তো ভুলের if-এ ভরে যায়। কতটা যাচাই “অতিরিক্ত”?

প্রশ্নটি খুব বাস্তব। প্রতিটি ফাংশনের শুরুতে দশটি যাচাই থাকলে আসল যুক্তিটাই হারিয়ে যায়, আর একই যাচাই পাঁচ জায়গায় থাকলে কোনটি সঠিক তা নিয়ে বিভ্রান্তি হয়। কয়েকটি নীতি সীমা টানতে সাহায্য করে:

  • বাইরের তথ্য বনাম ভেতরের চুক্তি: যা প্রোগ্রামের বাইরে থেকে আসে (ব্যবহারকারী, ফাইল, নেটওয়ার্ক, অন্য প্রোগ্রাম) — সেটি সীমানায় একবার পুরোপুরি যাচাই। এরপর ভেতরের ফাংশনগুলো ধরে নিতে পারে তথ্য বৈধ; সেখানে শুধু সস্তা assert.
  • যা ব্যর্থ হতে পারে, তা অবশ্যই: fopen, malloc, system call — এদের ফেরত-মান যাচাই “অতিরিক্ত” নয়, ন্যূনতম। কিন্তু নিজের লেখা max(a, b)-এর ফল যাচাই অর্থহীন।
  • যাচাই করে কী করবেন জানেন তো? একটি যাচাই যদি ভুল ধরে কিন্তু তারপর কী করতে হবে জানা না থাকে, তাহলে সেটি নিচের স্তরে নয় — ভুলটি ওপরে পাঠান।
  • বিরক্তিকর পুনরাবৃত্তি কমান: একই রকম যাচাই বারবার লাগলে একটি সাহায্যকারী ফাংশন বানান — যেমন এই পর্বের parse_long. অনেক প্রজেক্টে xmalloc নামে একটি wrapper থাকে, যা ব্যর্থ হলে বার্তা দিয়ে থামে — ছোট command-line টুলে এটা গ্রহণযোগ্য সরলীকরণ।
  • ঝুঁকি অনুযায়ী মাত্রা: একটি গেমের স্কোরবোর্ড আর একটি ব্যাংকের লেনদেন-ইঞ্জিনে যাচাইয়ের গভীরতা এক হবে না।

আরেকটি কৌশল হলো নকশা দিয়েই অবৈধ অবস্থা অসম্ভব করা — যেমন ঋণাত্মক হতে পারবে না এমন মানের জন্য সঠিক টাইপ বা enum ব্যবহার, যাতে যাচাইয়ের প্রয়োজনই কমে যায়।

মূল উপলব্ধি: লক্ষ্য “সব জায়গায় সব যাচাই” নয়, বরং “প্রতিটি ভুল ঠিক একবার, ঠিক জায়গায় ধরা, আর ঠিক সেই স্তরে সামলানো যে সিদ্ধান্ত নিতে পারে”।

প্র ০৫AI-কে দিয়ে কোড লেখালে সেটি কীভাবে “ভুলের পথ”-ও ঠিকমতো সামলাবে — শুধু সহজ পথ নয়?

AI-এর কোড সাধারণত “happy path”-এ নিখুঁত, কারণ আপনার অনুরোধে সেটুকুই চাওয়া হয়েছিল। ভুলের পথ পেতে হলে সেটা স্পষ্টভাবে চাইতে হবে, এবং তারপর যাচাই করতে হবে। একটি কার্যকর পদ্ধতি:

  • আগে চুক্তি, পরে কোড: ফাংশনের signature, ফেরত-মানের নিয়ম (0/-1, errno) আর “exit() নয়” — এগুলো নিজে ঠিক করে AI-কে দিন। নিয়ম আপনার প্রজেক্টের, AI-এর পছন্দের নয়।
  • ব্যর্থতার তালিকা চেয়ে নিন: “এই ফাংশন কত রকমভাবে ব্যর্থ হতে পারে, তালিকা দাও।” তারপর মিলিয়ে দেখুন কোডে প্রতিটির জন্য পথ আছে কি না। এটি আসলে একটি ছোট failure-mode analysis.
  • প্রতিটি ব্যর্থতার জন্য test: ফাইল নেই, ফাইল খালি, অনুমতি নেই, ভুল ফরম্যাট, বিশাল সংখ্যা। মেমরি-ব্যর্থতার জন্য এই পর্বের fail_at-এর মতো কৃত্রিম ব্যর্থতা (fault injection) ব্যবহার করুন।
  • যন্ত্র দিয়ে পাহারা: -Wall -Wextra, ফেরত-মান উপেক্ষার warning, AddressSanitizer বা Valgrind দিয়ে ভুলের পথ চালানো — leak সেখানেই লুকিয়ে থাকে।
  • দ্বিতীয় দৃষ্টি: আরেকটি নতুন কথোপকথনে AI-কে reviewer হিসেবে দিন: “শুধু ভুলের পথগুলো পর্যালোচনা করো — কোথায় leak, কোথায় ফেরত-মান উপেক্ষিত, কোথায় বার্তা অস্পষ্ট।” তবে চূড়ান্ত যাচাই নিজের test দিয়েই।

খেয়াল করুন, এখানে আপনার ভূমিকা কোড টাইপ করা নয় — সিস্টেমের নকশাকার ও পরীক্ষকের। কোথায় ব্যর্থতা সম্ভব আর কোন স্তর সিদ্ধান্ত নেবে, তা ঠিক করার জন্য পুরো প্রোগ্রামের প্রসঙ্গ লাগে, যা AI-এর কাছে থাকে না।

মূল উপলব্ধি: AI দ্রুত “সব ঠিক থাকলে” কোড দেয়; “কিছু ভুল হলে” কী হবে, সেই প্রশ্ন করা আর উত্তর যাচাই করা প্রকৌশলীর কাজ। ভবিষ্যতে এই দক্ষতাই আপনাকে আলাদা করবে।

অনুশীলন

এগুলোর বেশিরভাগে কোড লিখতে হবে না — ভুলের পথ নিয়ে ভাবাই আসল কাজ।

  1. ভুলের তালিকা: একটি প্রোগ্রাম ব্যবহারকারীর কাছ থেকে ফাইলের নাম নেয়, ফাইলটি খোলে, প্রতিটি লাইনের সংখ্যা যোগ করে, আর ফল result.txt-এ লেখে। কোথায় কোথায় কী কী ভুল হতে পারে? প্রতিটিকে programmer error বা runtime error হিসেবে চিহ্নিত করুন।

    Runtime error: ইনপুট পড়া যায়নি (fgets দিল NULL); ফাইলটি নেই বা পড়ার অনুমতি নেই; ফাইল খালি; কোনো লাইনে সংখ্যার বদলে লেখা বা বাড়তি অক্ষর; সংখ্যা long-এর সীমার বাইরে; যোগফল overflow; result.txt লেখার অনুমতি নেই; ডিস্ক ভরা (fprintf বা fclose ব্যর্থ)।

    Programmer error: buffer-এর আকার ভুল হিসাব; ফাইল বন্ধ করতে ভুলে যাওয়া; NULL pointer যাচাই ছাড়া ব্যবহার; ভুল format specifier. এগুলো ধরে assert, warning ও test. খেয়াল করুন, runtime error-এর তালিকাই লম্বা।

  2. পরীক্ষা করে দেখুন: উপরের parse.c-এ আরও তিনটি ইনপুট যোগ করুন: " 42" (শুরুতে স্পেস), "-7" আর শেষে newline সহ "42\n" — ফল কী এলো? কোনটি আপনার প্রত্যাশার সাথে মেলে না, আর কেন?

    " 42" → OK 42, কারণ strtol শুরুর whitespace নিজেই বাদ দেয়। "-7" → OK -7, চিহ্নসহ সংখ্যা বৈধ। কিন্তু "42\n" → “not a number”! কারণ শেষের newline বাড়তি অক্ষর হিসেবে গণ্য হলো। fgets দিয়ে লাইন পড়লে শেষে \n থাকে — তাই হয় আগে newline কেটে ফেলুন, নয়তো *end-এর পরে শুধু whitespace থাকলে মেনে নিন। এটাই সেই ধরনের ছোট ব্যাপার, যা happy path-এর পরীক্ষায় কখনো ধরা পড়ে না।

  3. সিদ্ধান্ত নিন: নিচের প্রতিটির জন্য বলুন — assert, সামলানো (কীভাবে), নাকি fail fast: (ক) একটি ফটো-এডিটরে থাম্বনেইল বানাতে মেমরি পাওয়া গেল না (খ) আপনার নিজের sort ফাংশনে n ঋণাত্মক এলো (গ) মোবাইল অ্যাপ সার্ভারের সাথে সংযোগ পাচ্ছে না (ঘ) ব্যাংকের হিসাবের ফাইল পড়তে গিয়ে দেখা গেল ফাইলের checksum মিলছে না।

    (ক) সামলানো: থাম্বনেইল ঐচ্ছিক সুবিধা — একটি সাধারণ আইকন দেখিয়ে বাকি অ্যাপ চালু রাখুন। এটাই graceful degradation.

    (খ) assert: আপনার নিজের কোড থেকে ঋণাত্মক আকার আসা মানে bug.

    (গ) সামলানো: কিছুক্ষণ পরপর retry (প্রতিবার অপেক্ষা বাড়িয়ে), এর মধ্যে আগে সংরক্ষিত তথ্য দেখান, আর ব্যবহারকারীকে জানান “অফলাইন”।

    (ঘ) fail fast: নষ্ট হিসাবের ওপর ভিত্তি করে লেনদেন চালানো ভয়ংকর। স্পষ্ট বার্তা, লগে বিস্তারিত, ব্যর্থতার exit status — আর মানুষের হস্তক্ষেপ।

  4. কোড লিখুন (ঐচ্ছিক): একটি ফাংশন লিখুন int copy_file(const char *src, const char *dst), যা একটি ফাইল আরেকটিতে কপি করে। দুটি ফাইল খোলা আর একটি buffer — তিনটি রিসোর্স। goto cleanup ব্যবহার করুন, আর প্রতিটি ব্যর্থতার পথ (দ্বিতীয় ফাইল খোলা না যাওয়া, লেখায় ভুল, fclose ব্যর্থ) পরীক্ষা করুন।

    কাঠামোটি এমন হবে: শুরুতে int rc = -1; FILE *in = NULL, *out = NULL; char *buf = NULL; তারপর in খুলুন, ব্যর্থ হলে goto cleanup; out খুলুন, একইভাবে; buf = malloc(...), একইভাবে। লুপে fread/fwrite; fwrite কম লিখলে goto cleanup. লুপ শেষে ferror(in) যাচাই করুন। সব ঠিক থাকলে rc = 0.

    cleanup:-এ উল্টো ক্রমে: free(buf); তারপর if (out && fclose(out) != 0) rc = -1; — খেয়াল করুন, লেখার ফাইলের fclose ব্যর্থ হলে পুরো কপিই ব্যর্থ, কারণ শেষ অংশ হয়তো ডিস্কে যায়নি। শেষে if (in) fclose(in); আর return rc;. তারপর প্রতিটি ব্যর্থতার পথ Valgrind দিয়ে চালিয়ে দেখুন।

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

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

ক্রমএকটি ভুল ঘটা থেকে সামলানো পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. fopen ব্যর্থ হয়ে NULL দেয়, errno-তে কারণ রাখে
  2. ডাকা ফাংশন ফেরত-মান যাচাই করে ভুল শনাক্ত করে
  3. এ পর্যন্ত নেওয়া রিসোর্স উল্টো ক্রমে ছাড়ে
  4. -1 ফেরত দিয়ে ওপরের স্তরকে জানায়
  5. সবচেয়ে ওপরের স্তর সিদ্ধান্ত নেয়: আবার চেষ্টা, default, নাকি থামা

নিচের কোনটি runtime error, programmer error নয়?

  • নিজের লুপ থেকে array-তে index -1 পাঠানো
  • ব্যবহারকারী যে ফাইলের নাম দিলেন, সেটি ডিস্কে নেই
  • free করার পরে আবার সেই pointer ব্যবহার করা
  • printf-এ long-এর জন্য %d লেখা

errno পড়ার সঠিক নিয়ম কোনটি?

  • যেকোনো ফাংশন ডাকার পরে errno শূন্য না হলে ধরে নিন ভুল হয়েছে
  • প্রোগ্রামের শেষে একবার errno দেখলেই যথেষ্ট
  • আগে ফেরত-মান দেখে ভুল নিশ্চিত করুন, তারপর সাথে সাথে errno পড়ুন
  • ফেরত-মান না দেখে শুধু errno দেখলেই চলে

strtol("12x", &end, 10) ডাকার পরে কী সত্য?

  • ফল 12, আর end দেখায় 'x'-কে — তাই বাড়তি অক্ষর ধরা যায়
  • ফল 0, কারণ লেখাটি পুরো সংখ্যা নয়
  • errno-তে ERANGE বসে
  • প্রোগ্রাম crash করে

একটি ফাংশন ফাইল খুলেছে, তারপর buffer নিয়েছে; এখন দ্বিতীয় malloc ব্যর্থ। সঠিক কাজ কোনটি?

  • সাথে সাথে return -1;
  • exit(1) ডেকে প্রোগ্রাম বন্ধ করা
  • ভুল উপেক্ষা করে কাজ চালিয়ে যাওয়া
  • goto দিয়ে cleanup-এ গিয়ে buffer ছেড়ে, ফাইল বন্ধ করে, -1 ফেরত দেওয়া

assert(fopen("data.txt", "r") != NULL); — এই লাইনে সমস্যা কী?

  • assert-এর ভেতরে ফাংশন ডাকা যায় না, তাই compile হবে না
  • দুটো সমস্যা: release build-এ (NDEBUG) fopen ডাকাই হবে না, আর ফাইল না থাকা bug নয় যে assert দিয়ে ধরতে হবে
  • কোনো সমস্যা নেই — ফাইল না পেলে প্রোগ্রাম থামবে, এটাই ঠিক
  • শুধু এটুকু যে ফাইলটি পরে বন্ধ করা হয়নি

AI-এর লেখা একটি সাহায্যকারী ফাংশনে ফাইল খোলা না গেলে printf("Error\n"); exit(1); লেখা। এর সবচেয়ে বড় দুর্বলতা কোনটি?

  • ওপরের স্তরের সিদ্ধান্তের সুযোগ কেড়ে নেয়, আর বার্তাটি stdout-এ যায় ও কারণ বলে না
  • exit(1) ধীরে কাজ করে
  • exit status 1 ভুল, হওয়া উচিত 0
  • printf-এ বানান ভুল আছে

শব্দকোষ

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

মনে রাখুন

  1. Bug ঠিক করতে হয় (assert, test); runtime error সামলাতে হয় (return code, errno দিয়ে)। দুটো আলাদা যন্ত্র।
  2. যে ফাংশন ব্যর্থ হতে পারে, তার ফেরত-মান সবসময় যাচাই — fopen, malloc, scanf, এমনকি fclose. সংখ্যা পড়তে strtol + তিনটি যাচাই।
  3. নিচের স্তর খবর দেয়, ওপরের স্তর সিদ্ধান্ত নেয়। Library-র ভেতরে exit() নয়।
  4. ব্যর্থ হলে রিসোর্স উল্টো ক্রমে ছাড়ুন — goto cleanup-এ সব পথ এক জায়গায় মেলে।
  5. বার্তা stderr-এ, স্পষ্ট ও কারণসহ; ব্যর্থতায় শূন্য নয় এমন exit status; ভুল ফল দেওয়ার চেয়ে থেমে যাওয়া ভালো।

পরের পর্বে: ভুলের জন্য প্রস্তুত থাকতে শিখলেন। কিন্তু ভুল যখন হয়েই যায় — প্রোগ্রাম crash করে বা ভুল উত্তর দেয় — তখন কী করবেন? পরের পর্বে ডিবাগিংয়ের পদ্ধতি আর হাতিয়ার: gdb দিয়ে ধাপে ধাপে চালানো, sanitizer দিয়ে মেমরির ভুল ধরা, আর Valgrind দিয়ে leak খোঁজা।

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

  • পর্ব ৩১ · ডিবাগিং — gdb, sanitizer আর Valgrindপরবর্তী পর্বভুল হয়ে গেলে তা খুঁজে বের করার পদ্ধতি — gdb দিয়ে ধাপে ধাপে চালানো, sanitizer আর Valgrind দিয়ে মেমরির ভুল ধরা।
  • C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
  • Software Testing & QAগভীরে যানভুলের পথ পরীক্ষা, fault injection আর test-এর পরিকল্পনা কীভাবে করতে হয়।
  • Cybersecurityপাশের ট্র্যাকযাচাই না করা ইনপুট আর overflow কীভাবে নিরাপত্তা-ত্রুটিতে পরিণত হয়।
  • Ethics in Computing & AI Safetyপাশের ট্র্যাকTherac-25-এর মতো ঘটনা থেকে প্রকৌশলীর দায়িত্ব ও নিরাপদ সিস্টেমের নীতি।
এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান