এই পর্বে যা শিখবেন
- কোন ভুল ধরে রাখবেন আর কোন ভুলে process থামাবেন, সেটি ঠিক করা
- Express ৪ আর ৫-এ async error-এর আসল পার্থক্য, সত্যিকারের run থেকে
AppError, 404 catch-all আর একটি স্থির JSON আকার দিয়ে central handler বানানো- Validation, unique violation আর timeout-কে সঠিক status-এ বদলানো
- Timeout, retry আর graceful shutdown-এর সংখ্যা হিসাব করা
- Server-এর error কোড React-এর UI-তে ফেরানো
১ · দুই রকম ভুল: প্রত্যাশিত সমস্যা আর কোডের বাগ
রেস্তোরাঁয় আপনি বিরিয়ানি চাইলেন। ওয়েটার বললেন, “আজ শেষ হয়ে গেছে, তেহারি আছে।” এটি স্বাভাবিক ঘটনা। রেস্তোরাঁ জানত এমন হতে পারে, উত্তরও তৈরি ছিল। এবার ভাবুন রান্নাঘরে গ্যাসের লাইন ফেটে গেছে। ওয়েটার তখন খদ্দেরকে পাইপের নকশা বোঝাবেন না, হাসিমুখে অর্ডারও নিতে থাকবেন না। তিনি বলবেন “আজ বন্ধ”, আর ভেতরে মিস্ত্রি ডাকা হবে।
Backend-এও ঠিক এই দুই ভাগ। পর্ব ২৬-এ আপনি middleware-এর সারি দেখেছেন। পর্ব ২৭-এ দেখেছেন ইনপুট যাচাই। এই পর্বে দেখব সারির কোথাও গোলমাল হলে কী হয়।
Operational error
কোড ঠিক আছে, কিন্তু বাইরের জগৎ যা চেয়েছিল তা হয়নি। পণ্য নেই, SKU আগেই আছে, ইনপুট ভুল, পেমেন্ট সার্ভিস সাড়া দিচ্ছে না।
উত্তর: সঠিক status, পরিষ্কার বার্তাProgrammer bug
কোড নিজেই ভুল। null-এর property পড়া, SQL-এ টেবিলের নামের বানান ভুল, ভুল argument পাঠানো।
এই ভাগ কেন দরকার? একই catch দিয়ে সব ধরলে দুটি ভুল হয়। Bug-কে “পণ্য পাওয়া যায়নি” বলে চালিয়ে দিলে কেউ টের পান না যে কোড ভাঙা। আবার ভুল ইনপুটকে 500 বললে alert বাজে অকারণে, আর ব্যবহারকারী জানেন না কী বদলাবেন।
একই error সবসময় একই ভাগে পড়ে না। ফাইল না পাওয়া ব্যবহারকারীর আপলোডে operational, কিন্তু app চালুর সময় নিজের config ফাইল না পেলে সেটি deployment-এর ভুল। প্রশ্নটি হলো: এই ঘটনা আমরা আগে ভেবে রেখেছি কি না, আর এরপর process-এর অবস্থা জানা আছে কি না।
JavaScript-এর try/catch, throw আর exception কীভাবে ছড়ায়, তা JavaScript কোর্সের error handling পর্বে আছে। এখানে সেগুলো ধরে নিয়ে server-এর দিকে তাকাব।
Express তৈরি হয় ২০১০ সালে, TJ Holowaychuk-এর হাতে। তখন Node.js-এ async মানে ছিল callback, তাই error middleware বানানো হয়েছিল next(err) ডাকার জন্য। Promise আর async/await পরে এলেও Express ৪ সেগুলো দেখত না। Express ৫.০ এসেছে ২০২৪ সালে, আর এটিই প্রথম major সংস্করণ যা নিজে থেকে rejected promise ধরে। লেখার সময়ে npm-এর latest হলো 5.3.0, আর ৪-এর ধারায় 4.22.3.
২ · Error কীভাবে ছড়ায়: sync, async আর Express ৪ বনাম ৫
একটি synchronous handler-এ throw করলে Express সেটি ধরে, কারণ handler ডাকার লাইনটি তার নিজের try/catch-এর ভেতরে। Async হলে গল্প বদলায়। async ফাংশন throw করলে কিছুই ওপরে ওঠে না। সে শুধু একটি rejected promise ফেরত দেয়। সেই promise কেউ দেখলে ভালো, না দেখলে সমস্যা।
নিচের block-এ Express নেই, কারণ Wandbox-এ npm package চলে না। এটি node:http দিয়ে বানানো ছোট মডেল। একই handler দুভাবে ডাকা হয়েছে। চালিয়ে দেখুন।
const http = require('node:http');
// Express 4-এর মতো: handler-এর ফেরত দেওয়া promise কেউ দেখে না
const plain = (fn) => (req, res) => { fn(req, res); };
// Express 5-এর মতো: promise ধরা হয়, ভুল যায় একটি কেন্দ্রীয় জায়গায়
const wrap = (fn) => (req, res) =>
Promise.resolve().then(() => fn(req, res)).catch((err) => {
console.log(' central handler:', err.message);
res.statusCode = 500;
res.end('{"error":"internal"}');
});
async function getProduct() { await null; throw new Error('db down'); }
// এই hook না থাকলে প্রথম চেষ্টাতেই পুরো process মরে যেত
process.on('unhandledRejection', (r) => console.log(' unhandledRejection:', r.message));
const get = (url) => new Promise((resolve, reject) => {
http.get(url, { signal: AbortSignal.timeout(200) }, (res) => {
let body = '';
res.on('data', (c) => { body += c; });
res.on('end', () => resolve(`${res.statusCode} ${body}`));
}).on('error', reject);
});
async function tryIt(name, handler) {
const server = http.createServer(handler);
await new Promise((r) => server.listen(0, r));
console.log(name);
try {
console.log(' client got:', await get(`http://127.0.0.1:${server.address().port}/`));
} catch (e) {
console.log(' client gave up:', e.name);
}
server.closeAllConnections();
server.close();
}
tryIt('without wrapper', plain(getProduct))
.then(() => tryIt('with wrapper', wrap(getProduct)));আউটপুট:
without wrapper
unhandledRejection: db down
client gave up: AbortError
with wrapper
central handler: db down
client got: 500 {"error":"internal"}
প্রথম ক্ষেত্রে কেউ res.end ডাকেনি। Client ২০০ ms অপেক্ষা করে নিজেই অনুরোধ বাতিল করেছে (AbortError). এটিই “ঝুলে থাকা request”. দ্বিতীয় ক্ষেত্রে .catch ভুলটি ধরে উত্তর পাঠিয়েছে। Hook সরিয়ে নিলে প্রথম ক্ষেত্রে ঝুলে থাকা নয়, পুরো process পড়ে যায়, যা নিচের সত্যিকারের run-এ দেখবেন।
একই ফাইল, দুই Express: সত্যিকারের run
এবার আসল জিনিস। একটি ফাইলে ছয় রকম route আর চার argument-এর একটি error handler লেখা হলো। সেটি Express 4.22.3 আর 5.3.0 দিয়ে চালানো হলো, Node 24.15.0-তে। Route-গুলোর মূল অংশ:
app.get('/sync', (req, res) => { throw new Error('sync boom'); });
app.get('/async', async (req, res) => {
await null;
throw new Error('async boom');
});
app.get('/reject-empty', () => Promise.reject());
app.get('/floating', (req, res) => {
Promise.reject(new Error('floating boom')); // return নেই
});
app.get('/timer', (req, res) => {
setTimeout(() => { throw new Error('timer boom'); }, 5);
});
app.get('/callback', (req, res, next) => {
fs.readFile('no-such-file.txt', (err, data) => {
if (err) return next(err);
res.send(data);
});
});
app.use((err, req, res, next) => {
console.error('error handler got:', err.message);
res.status(500).json({ error: 'internal' });
});Express ৪-এ /async-এর সত্যিকারের run (path বদলে ~/bazarghar করা, লেখা ছোট করা):
$ curl -i http://127.0.0.1:47281/async
[server] ~/bazarghar/compare.cjs:17
throw new Error('async boom');
^
Error: async boom
at ~/bazarghar/compare.cjs:17:9
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
Node.js v24.15.0
curl: (56) Recv failure: Connection was reset
[server exited code=1 signal=null]
আর Express ৫-এ একই ফাইল, একই অনুরোধ:
$ curl -i http://127.0.0.1:47283/async
[server] error handler got: async boom
HTTP/1.1 500 Internal Server Error
Content-Type: application/json; charset=utf-8
{"error":"internal"}
একটি অনুরোধ দিয়ে পুরো server নামিয়ে দেওয়া গেল Express ৪-এ, যদি handler-এ ধরা না থাকে। ছয়টি route-এর পুরো ফল:
| Route | Express 4.22.3 | Express 5.3.0 |
|---|---|---|
/sync | 500, handler চলল | 500, handler চলল |
/async | process মরল, exit 1 | 500, handler চলল |
/reject-empty | process মরল, exit 1 | 500, বার্তা Rejected promise |
/floating | process মরল, exit 1 | process মরল, exit 1 |
/timer | process মরল, exit 1 | process মরল, exit 1 |
/callback | 500, next(err) দিয়ে | 500, next(err) দিয়ে |
টেবিলের নিচের দিকটিই আসল শিক্ষা। Express ৫ কোনো জাদু করে না। সে শুধু handler-এর ফেরত দেওয়া promise-এর দিকে তাকায়। অফিসিয়াল গাইডও বলছে, promise ফেরত না দিলে Express তার অস্তিত্বই জানে না। তাই তিনটি নিয়ম:
- Handler-কে
asyncরাখুন আর প্রতিটি promise-এawaitদিন। - Callback API হলে
next(err)নিজে ডাকুন, অথবাfs.promisesধরনের promise সংস্করণ নিন। - Timer বা event-এর callback-এর ভেতরে
try/catchসেই callback-এর ভেতরেই লিখুন।
Express ৪ এখনো চালাতে হলে একটি ছোট wrapper যথেষ্ট। Express ৫-এ এটি মুছে ফেলুন, এর আর কাজ নেই।
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/async', asyncHandler(async (req, res) => { /* … */ }));Express ৪-এ unhandledRejection hook বসালে process বাঁচে, কিন্তু request ঝুলে থাকে। সত্যিকারের run-এ curl: (28) Operation timed out after 2015 milliseconds with 0 bytes received এসেছে। মরে যাওয়া আর ঝুলে থাকা, দুটিই খারাপ; hook দিয়ে এর সমাধান হয় না।
৩ · চার argument-এর error middleware: জায়গা, ক্রম আর 404
ডাকঘরে চিঠি বাছাইয়ের সারি ভাবুন। ঠিকানা ভুল হলেই চিঠি সারি থেকে সরে যায় “ফেরত” টেবিলে। মাঝের বাকি টেবিলগুলোয় আর যায় না। Express-এর error পথও তাই। next(err) বা throw হলেই সে সাধারণ middleware-গুলো ডিঙিয়ে পরের চার argument-এর ফাংশনে চলে যায়।
স্বাভাবিক পথ আর error-এর পথ
Normal flow and the error lane in an Express app
এই আকৃতি পাওয়ার জন্য app.js-এর শেষের অংশটি সবসময় এমন:
// … সব middleware আর route ওপরে
app.use(notFound); // কোনো route না মিললে এখানে আসে
app.use(errorHandler); // একদম শেষে, চার argumentExpress একটি ফাংশনকে error middleware হিসেবে চেনে শুধু তার argument-এর সংখ্যা দেখে। চারটি থাকলেই সে error পথের। তাই ব্যবহার না করলেও next লিখতে হবে। দুটি ভুল আমরা ইচ্ছা করে করে দেখেছি (Express 5.3.0, NODE_ENV=production, সত্যিকারের run):
| ভুল | যা ঘটল |
|---|---|
| Handler route-গুলোর আগে বসানো | Custom handler চলেনি। উত্তর এলো Express-এর নিজের HTML পাতা: Internal Server Error |
তিন argument: (err, req, res) | Error-এ চলেনি। অজানা ঠিকানায় সাধারণ middleware হিসেবে চলে TypeError: res.status is not a function |
দ্বিতীয় ভুলটি মজার। ফাংশনটি তিন argument-এর সাধারণ middleware হয়ে গেছে। তাই err নামে এসেছে req, আর res নামে এসেছে next. নাম আপনার, অবস্থান Express-এর।
এখন একটি ভুলের পুরো যাত্রা দেখুন। অনুরোধটি বাজারঘরে এমন পণ্য চেয়েছে, যা নেই। “পরে” চেপে এগোন।
- Request
/products/999এলো - Middleware
req.idসিল দিল - Handler
throwপণ্য নেই - Router
next(err)পথ বদল - errorHandler
404উত্তর গেল
$ curl -i http://127.0.0.1:47301/api/products/999
app.use((req, res, next) => {
req.id = randomUUID().slice(0, 8); // 34d3376d
next();
});
app.use(express.json({ limit: '1kb' }));
const row = db.prepare(...).get(999); // undefined if (!row) throw new AppError(404, 'PRODUCT_NOT_FOUND', 'এই পণ্যটি পাওয়া যায়নি');
handler ফেরত দিল: Promise { <rejected> AppError }
router ডাকল: next(err)
notFound(req, res, next) 3টি: বাদ errorHandler(err, req, res, next) 4টি: এটিই
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
{"error":{"code":"PRODUCT_NOT_FOUND",
"message":"এই পণ্যটি পাওয়া যায়নি",
"requestId":"34d3376d"}}
- অনুরোধ এলো।
GET /api/products/999. এখনো কোনো ভুল নেই, সাধারণ অনুরোধ। - স্বাভাবিক সারি। Request id বসল, JSON middleware পার হলো। দুজনই
next()ডাকল, কোনো argument ছাড়া। - Handler throw করল। Database-এ row নেই, তাই
AppErrorছোড়া হলো। ফাংশনটিasync, তাই এই throw আসলে একটি rejected promise. - Router ধরল। Express 5 handler-এর ফেরত দেওয়া promise দেখছিল। Reject হতেই সে
next(err)ডাকল। সারি এখন error পথে। - Argument গোনা।
notFound-এর তিনটি argument, তাই সে বাদ।errorHandler-এর চারটি, তাই এখানেই থামল। - এক জায়গা থেকে উত্তর। Status 404, স্থির
code, নিরাপদ বার্তা আরrequestId. Log-এ এই id-টিই আছে, তাই ব্যবহারকারীর দেওয়া id দিয়ে ঘটনাটি খুঁজে পাওয়া যায়।
একটি error middleware next(err) ডেকে পরের error middleware-এ পাঠাতে পারে। যেমন প্রথমটি log লেখে, দ্বিতীয়টি উত্তর দেয়। শেষ পর্যন্ত কেউ উত্তর না দিলে Express-এর নিজের default handler দেয়। আর উত্তরের অর্ধেক পাঠানোর পরে error হলে res.headersSent সত্য হয়; তখন status বদলানো যায় না, তাই next(err) দিয়ে default handler-কে দিতে হয়। আমাদের run-এ /api/report অর্ধেক JSON পাঠিয়ে থেমেছে: status ছিল 200, আর curl বলেছে (18) transfer closed with outstanding read data remaining. অর্থাৎ সংযোগ ভেঙে client-কে জানানো হলো যে উত্তর অসম্পূর্ণ।
৪ · AppError আর একটি স্থির JSON আকার
সাধারণ Error-এর শুধু বার্তা থাকে। Handler-এর জানা দরকার আরও দুটি জিনিস: কোন status, আর কোন যন্ত্রপাঠ্য code. তাই নিজের একটি class. বাজারঘরের কোডে এটি একটি ফাইলেই থাকে।
export class AppError extends Error {
constructor(status, code, message, extra = {}) {
super(message, { cause: extra.cause });
this.name = 'AppError';
this.status = status;
this.code = code;
this.fields = extra.fields;
this.retryAfter = extra.retryAfter;
}
}
export function notFound(req, res, next) {
next(new AppError(404, 'ROUTE_NOT_FOUND', 'এই ঠিকানায় কিছু নেই'));
}message বদলাতে পারে, অনুবাদ হতে পারে। code বদলায় না। Frontend code দেখে সিদ্ধান্ত নেয়, বার্তার লেখা মিলিয়ে নয়। cause রাখে মূল ভুলটি, যাতে log-এ আসল কারণ থাকে। আর সব উত্তরের আকার এক:
{
"error": {
"code": "PRODUCT_NOT_FOUND", // স্থির, যন্ত্রের জন্য
"message": "এই পণ্যটি পাওয়া যায়নি", // মানুষের জন্য
"requestId": "34d3376d", // log খোঁজার সূত্র
"fields": [ … ] // শুধু ঘর-ভিত্তিক ভুলে
}
}
এবার চালিয়ে দেখুন, একই তিনটি ভুল অসাবধান handler আর নিরাপদ handler কীভাবে সামলায়।
class AppError extends Error {
constructor(status, code, message) {
super(message);
this.name = 'AppError';
this.status = status;
this.code = code;
}
}
// খারাপ: যা ভুল, হুবহু তাই client-কে
const leaky = (err) => ({
status: err.status ?? 500,
message: err.message,
stackHasPaths: /[\\/]/.test(err.stack),
});
// ভালো: চেনা ভুলের বার্তা যায়, অচেনা ভুলে একটি সাধারণ বাক্য
const safe = (err) => err instanceof AppError
? { status: err.status, code: err.code, message: err.message }
: { status: 500, code: 'INTERNAL', message: 'Something went wrong on our side' };
function bug() {
const coupon = null;
return coupon.percent; // সত্যিকারের TypeError
}
const sqlErr = new Error(
'relation "prodcts" does not exist: SELECT * FROM prodcts WHERE id = 7');
const errors = [new AppError(404, 'PRODUCT_NOT_FOUND', 'Product not found'), sqlErr];
try { bug(); } catch (e) { errors.push(e); }
for (const err of errors) {
console.log(err.name);
console.log(' leaky:', JSON.stringify(leaky(err)));
console.log(' safe :', JSON.stringify(safe(err)));
}দ্বিতীয় ভুলের আউটপুট লক্ষ করুন:
Error
leaky: {"status":500,"message":"relation \"prodcts\" does not exist: SELECT * FROM prodcts WHERE id = 7","stackHasPaths":true}
safe : {"status":500,"code":"INTERNAL","message":"Something went wrong on our side"}
অসাবধান handler টেবিলের নাম আর পুরো SQL বাইরে দিয়ে দিয়েছে, আর stackHasPaths:true বলছে stack-এ ফাইলের path আছে। নিরাপদ handler দুই অচেনা ভুলে হুবহু একই উত্তর দিয়েছে। প্রথম ভুল (AppError) দুই দিকেই একই বার্তা পেয়েছে, কারণ সেটি আমরা ভেবেই লিখেছি।
মান হিসেবে: RFC 9457 problem details
নিজের আকার বানানোর বদলে একটি প্রকাশিত মানও নেওয়া যায়। Problem Details এসেছিল RFC 7807 (২০১৬) হিসেবে, আর জুলাই ২০২৩-এ RFC 9457 তার জায়গা নিয়েছে। Media type application/problem+json; মূল সদস্য type, title, status, detail, instance. বাড়তি সদস্য যোগ করা যায়, আর client অচেনা সদস্য উপেক্ষা করবে। type না থাকলে ধরে নেওয়া হয় about:blank. একই app-এ ERROR_FORMAT=problem দিয়ে চালানো সত্যিকারের run:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
{"type":"https://bazarghar.example/problems/product_not_found","title":"এই পণ্যটি পাওয়া যায়নি","status":404,"instance":"/api/products/999","code":"PRODUCT_NOT_FOUND","requestId":"157c7435"}
কোনটি নেবেন? Public API বা অনেক দলের ব্যবহারের API হলে মান নেওয়ার যুক্তি শক্ত, কারণ টুল আর client আগে থেকেই আকারটি চেনে। শুধু নিজের frontend-এর জন্য হলে ছোট নিজস্ব আকারও ঠিক আছে। আসল নিয়ম একটাই: একটি আকার বেছে সব জায়গায় সেটিই রাখা। এই পর্বে নিজস্ব আকার রাখছি; বিস্তারিত তুলনা পর্ব ৩৩-এ।
৫ · চেনা ভুলকে সঠিক উত্তরে বদলানো
সব ভুল আপনার AppError হয়ে আসে না। Zod ছোড়ে ZodError, database ছোড়ে নিজের error, express.json ছোড়ে নিজের। কেন্দ্রীয় handler-এর কাজ এদের চিনে একটি ভাষায় আনা। অনেকটা হোটেলের রিসেপশনের মতো: ভেতরের বিভাগ যে ভাষাতেই বলুক, অতিথি শোনেন একই ধরনের ভদ্র বাক্য।
import { ZodError } from 'zod';
export function toAppError(err) {
if (err instanceof AppError) return err;
if (err instanceof ZodError) {
const fields = err.issues.map((i) => ({
path: i.path.join('.'), message: i.message,
}));
return new AppError(422, 'VALIDATION_FAILED',
'কিছু ঘরের তথ্য ঠিক নেই', { fields, cause: err });
}
if (err.type === 'entity.parse.failed') {
return new AppError(400, 'MALFORMED_JSON',
'পাঠানো JSON পড়া গেল না', { cause: err });
}
if (err.type === 'entity.too.large') {
return new AppError(413, 'BODY_TOO_LARGE',
'পাঠানো তথ্য সীমার চেয়ে বড়', { cause: err });
}
if (err.code === 'ERR_SQLITE_ERROR' && err.errcode === 2067) {
return new AppError(409, 'DUPLICATE_SKU', 'এই SKU আগেই আছে', {
fields: [{ path: 'sku', message: 'এই SKU আগেই আছে' }], cause: err,
});
}
if (err.name === 'TimeoutError') {
return new AppError(504, 'UPSTREAM_TIMEOUT',
'পেমেন্ট সার্ভিস সময়মতো সাড়া দেয়নি', { cause: err, retryAfter: 5 });
}
if (err.message === 'fetch failed') {
return new AppError(503, 'UPSTREAM_DOWN',
'পেমেন্ট সার্ভিসে এখন যাওয়া যাচ্ছে না', { cause: err, retryAfter: 30 });
}
return null; // অচেনা মানে bug
}Unique violation-এর code নির্ভর করে database আর library-র ওপর। উপরের 2067 হলো SQLite-এর SQLITE_CONSTRAINT_UNIQUE, কারণ lab-এ Node-এর built-in SQLite চলেছে। PostgreSQL-এ এটি SQLSTATE 23505, MySQL-এ error 1062, আর Prisma-য় P2002 (পর্ব ৩৯). SELECT করে দেখে তারপর INSERT করার বদলে unique constraint-এর ওপর ভরসা রাখা নিরাপদ, কারণ দুই অনুরোধ একসাথে এলে আগের যাচাই দুজনেই পাস করতে পারে (পর্ব ৪৩).
এবার errorHandler নিজে। চেনা হলে তার বার্তা, অচেনা হলে সাধারণ বাক্য। Log-এ সবসময় পুরো ঘটনা।
const isDev = process.env.NODE_ENV === 'development';
export function errorHandler(err, req, res, next) {
if (res.headersSent) return next(err);
const known = toAppError(err);
const status = known?.status ?? 500;
const code = known?.code ?? 'INTERNAL';
const message = known?.message ?? 'আমাদের দিকে একটি সমস্যা হয়েছে';
console.error(JSON.stringify({
level: status >= 500 ? 'error' : 'warn',
requestId: req.id, method: req.method, path: req.originalUrl,
status, code, name: err.name, message: err.message,
stack: status >= 500 ? err.stack : undefined,
}));
if (known?.retryAfter) res.set('Retry-After', String(known.retryAfter));
res.status(status).json({
error: {
code, message, requestId: req.id, fields: known?.fields,
stack: isDev ? err.stack : undefined,
},
});
}নিচের ডিবাগারে পাঁচটি আলাদা ভুল toAppError-এর ভেতর দিয়ে যাচ্ছে। সবুজ দাগ দেখাচ্ছে কোন লাইনে সিদ্ধান্ত হলো।
1function toAppError(err) {2 if (err instanceof AppError) return err;3 if (err instanceof ZodError) return E422;4 if (err.errcode === 2067) return E409;5 if (err.name === 'TimeoutError') return E504;6 return null; // অচেনা মানে bug7}
| err | |
| ধরন | |
| status | |
| code | |
| log level | |
| log-এ stack |
- নিজের ভুল। আমরা নিজেই
AppErrorছুড়েছি, তাই দ্বিতীয় লাইনেই ফিরল। Status 404, log levelwarn, কারণ এখানে সার্ভারের কিছু ভাঙেনি। - ইনপুটের ভুল। Zod আপত্তি করেছে। Status 422, সাথে কোন ঘরে কী ভুল। ব্যবহারকারী ঠিক করে আবার পাঠাতে পারবেন।
- দুইবার একই SKU. Database-এর নিজের বার্তা
UNIQUE constraint failed: products.skulog-এ রইল। Client পেল 409 আর সহজ বাক্য। - বাইরের সার্ভিস ধীর। এটি আমাদের কোডের ভুল নয়, তবু status 504 আর log level
error. কারণ বেশি ঘটলে কাউকে জানা দরকার, তাই stack-ও রাখা হয়েছে। - অচেনা, মানে bug. কোনো শর্ত মেলেনি।
nullফিরল, তাই status 500 আর সাধারণ বাক্য। Log-এ পুরো stack গেল। এটি ঠিক করতে হবে কোডে, বার্তায় নয়।
সত্যিকারের run-এ (Express 5.3.0, Zod 4.6.7) তিনটি অনুরোধের উত্তর:
$ curl -i -X POST -H content-type:application/json -d '{"sku":"x","price":-5}' …/api/products
HTTP/1.1 422 Unprocessable Entity
{"error":{"code":"VALIDATION_FAILED","message":"কিছু ঘরের তথ্য ঠিক নেই","requestId":"df83861b","fields":[{"path":"sku","message":"Too small: expected string to have >=3 characters"},{"path":"name","message":"Invalid input: expected string, received undefined"}, …]}}
$ curl -i -X POST -H content-type:application/json -d '{"sku":"RICE-5KG","name":"abc","price":10}' …/api/products
[server] {"level":"warn",…,"status":409,"code":"DUPLICATE_SKU","name":"Error","message":"UNIQUE constraint failed: products.sku"}
HTTP/1.1 409 Conflict
{"error":{"code":"DUPLICATE_SKU","message":"এই SKU আগেই আছে","requestId":"3428cc89","fields":[{"path":"sku","message":"এই SKU আগেই আছে"}]}}
$ curl -i -X POST -H content-type:application/json -d '{bad' …/api/products
HTTP/1.1 400 Bad Request
{"error":{"code":"MALFORMED_JSON","message":"পাঠানো JSON পড়া গেল না","requestId":"ce86c87e"}}
দ্বিতীয় উত্তরের [server] লাইনটি log, আর তার নিচেরটি client-এর উত্তর। SQL-এর ভাষা ভেতরে রইল, বাইরে গেল সহজ বাংলা। অজানা ঠিকানায় (/nope) এসেছে একই আকারে ROUTE_NOT_FOUND, আর প্রায় ২ KB-র body-তে (সীমা 1kb) 413 BODY_TOO_LARGE.
৬ · Client-কে কী বলবেন না: stack, SQL, path আর NODE_ENV
Error বার্তা আক্রমণকারীর কাছে মানচিত্রের মতো। Stack trace বলে দেয় ফাইলের path, framework-এর নাম আর সংস্করণের ইঙ্গিত। SQL বলে দেয় টেবিল আর কলামের নাম। connect ECONNREFUSED ধরনের বার্তা বলে দেয় ভেতরের IP আর port. এসব বাইরে গেলে পরের আক্রমণ সহজ হয়, যেমন injection-এর জন্য সঠিক টেবিলের নাম জানা। তাই নিয়ম সহজ: পুরো সত্য log-এ, সংক্ষিপ্ত সত্য client-এ, আর দুটিকে জোড়ে requestId.
Express-এর default handler NODE_ENV দেখে আচরণ বদলায়। একই route, একই ভুল, দুটি সত্যিকারের run:
$ curl -i http://127.0.0.1:47291/boom
HTTP/1.1 500 Internal Server Error
Content-Security-Policy: default-src 'none'
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
<pre>Error: secret table prodcts missing<br> at ~/bazarghar/mistakes.mjs:9:38<br> at Layer.handleRequest (~/bazarghar/node_modules/router/lib/layer.js:152:17)<br> …</pre>
$ curl -i http://127.0.0.1:47292/boom
HTTP/1.1 500 Internal Server Error
Content-Security-Policy: default-src 'none'
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
<pre>Internal Server Error</pre>
উপরের আউটপুট ছোট করা, আর path বদলে ~/bazarghar লেখা। লক্ষ করুন, ফাঁসটি ঘটছে NODE_ENV সেট না থাকলে। সেট করতে ভুলে গেলে production-এও stack বাইরে যাবে। Express-এর এই default-এর ওপর নির্ভর করবেন না। নিজের handler-এ উল্টো নিয়ম নিন: শুধু NODE_ENV === 'development' হলে stack দিন। আমাদের run-এ NODE_ENV সেট না করে /api/bug/null চালালে উত্তরে stack আসেনি:
{"error":{"code":"INTERNAL","message":"আমাদের দিকে একটি সমস্যা হয়েছে","requestId":"09c0175f"}}
আর NODE_ENV=development-এ (নিজের ল্যাপটপে কাজের সুবিধার জন্য) stack এসেছে:
{"error":{"code":"INTERNAL","message":"আমাদের দিকে একটি সমস্যা হয়েছে","requestId":"e634043a","stack":"TypeError: Cannot read properties of null (reading 'percent')\n at ~/bazarghar/src/app.js:42:31\n at Layer.handleRequest (~/bazarghar/node_modules/router/lib/layer.js:152:17)\n …"}}
Log-ও নিরাপদ রাখুন। Password, token বা পুরো card নম্বর log-এ গেলে সেটি আরেকটি ফাঁস। Log-এর গঠন, requestId কীভাবে সব লাইনে আসে আর secret কীভাবে আড়াল করবেন, তা পর্ব ৩১-এ। তত্ত্বের জন্য Cybersecurity কোর্সের logging পর্ব দেখুন।
আরেকটি সূক্ষ্ম বিষয়: 500-এ বার্তা সাধারণ হলেও 4xx-এর বার্তাও ভেবে লিখতে হয়। “এই ইমেইলে অ্যাকাউন্ট নেই” বললে কার অ্যাকাউন্ট আছে তা জানা যায়। এজন্য login-এর ভুলে একই বার্তা দেওয়া ভালো (পর্ব ৪৯). অন্যের order চাইলে অনেক API 403-র বদলে 404 দেয়, যাতে order-টি আছে কি না সেটিও বোঝা না যায়।
৭ · Timeout আর ভেঙে পড়া dependency
বাজারঘরের চেকআউট একটি বাইরের পেমেন্ট সার্ভিসকে ডাকে। সেটি ধীর হলে কী হবে? রিকশা ডাকতে গিয়ে আপনি অনন্তকাল গলির মোড়ে দাঁড়িয়ে থাকবেন না। কিছুক্ষণ পরে অন্য ব্যবস্থা দেখবেন। Server-এরও সেই সীমা দরকার, আর সেটি আপনাকেই লিখতে হবে। fetch-এর নিজে থেকে কোনো ছোট সীমা নেই বলে ধরে নিন।
app.post('/api/pay', async (req, res) => {
const r = await fetch(`${GATEWAY}/charge`, {
method: 'POST',
signal: AbortSignal.timeout(300), // ৩০০ ms-এর বেশি নয়
});
res.json(await r.json());
});একটি নকল gateway বানানো হলো, যে ২ সেকেন্ড পরে উত্তর দেয়। আরেক run-এ gateway বন্ধ রাখা হলো। দুটি সত্যিকারের ফল (log-এর লাইন ছোট করা):
# gateway ধীর
[server] {"level":"error",…,"status":504,"code":"UPSTREAM_TIMEOUT","name":"TimeoutError","message":"The operation was aborted due to timeout"}
HTTP/1.1 504 Gateway Timeout
Retry-After: 5
{"error":{"code":"UPSTREAM_TIMEOUT","message":"পেমেন্ট সার্ভিস সময়মতো সাড়া দেয়নি","requestId":"eb85dcf8"}}
# gateway বন্ধ
[server] {"level":"error",…,"status":503,"code":"UPSTREAM_DOWN","name":"TypeError","message":"fetch failed"}
HTTP/1.1 503 Service Unavailable
Retry-After: 30
{"error":{"code":"UPSTREAM_DOWN","message":"পেমেন্ট সার্ভিসে এখন যাওয়া যাচ্ছে না","requestId":"b6496af5"}}
Retry-After header client-কে বলে কতক্ষণ পরে আবার চেষ্টা করা যুক্তিসঙ্গত। সংখ্যা দুটি এখানে উদাহরণ, আপনার সার্ভিস অনুযায়ী ঠিক করবেন। err.message === 'fetch failed' মেলানো ভঙ্গুর নিয়ম; বড় কোডে gateway client-এর ভেতরে ভুল ধরে নিজের AppError ছোড়াই পরিষ্কার (পর্ব ২৯). TimeoutError-এর আসল অর্থ ফলাফল অজানা। টাকা কাটা হয়েছে কি হয়নি, আমরা জানি না। তাই পেমেন্টে অন্ধভাবে আবার পাঠানো বিপজ্জনক (পর্ব ৬৫).
Timeout না থাকলে কত ক্ষতি: একটি হিসাব
একসাথে কতগুলো অনুরোধ আটকে থাকবে, তার হিসাব Little's law: L = λ × W, যেখানে λ হলো প্রতি সেকেন্ডে আসা অনুরোধ আর W হলো প্রতিটির অপেক্ষা। ধরুন /api/pay-তে সেকেন্ডে ৫০টি অনুরোধ আসে (ধরে নেওয়া সংখ্যা)।
| অবস্থা | W | L = 50 × W |
|---|---|---|
| Gateway আটকে গেছে, সীমা ৬০ সেকেন্ড | 60 s | 3000টি অনুরোধ একসাথে আটকে |
| Gateway আটকে গেছে, সীমা ৩০০ ms | 0.3 s | 15টি |
৩০০০টি খোলা অনুরোধ মানে ৩০০০টি সংযোগ, memory আর সম্ভবত database connection. একটি বাইরের সার্ভিসের সমস্যা তখন আপনার পুরো শপ নামিয়ে দেয়। একে বলে cascading failure.
Node-এর নিজের timeout-গুলো এই কাজ করে না। লেখার সময়ের ডকুমেন্টে server.requestTimeout-এর default ৩০০,০০০ ms আর server.headersTimeout-এর ৬০,০০০ ms; দুটিই client থেকে অনুরোধ পড়ার সীমা, আপনার handler কতক্ষণ ভাববে তার নয়। server.timeout-এর default 0, অর্থাৎ সীমা নেই। Handler-এর সীমা আপনার কোডের দায়িত্ব।
Retry: একটি ছোট নোট
ক্ষণিকের ভুলে আবার চেষ্টা কাজের, কিন্তু তিনটি শর্তে। প্রথমত, শুধু এমন কাজে, যা দুবার হলে ক্ষতি নেই (GET, অথবা idempotency key সহ POST). দ্বিতীয়ত, 4xx-এ নয়; ভুল ইনপুট আবার পাঠালেও ভুলই থাকবে। তৃতীয়ত, মোট সময় আর চেষ্টার সীমা থাকতে হবে। ৩০০ ms timeout-এ ৩ বার চেষ্টা, মাঝে ১০০ আর ২০০ ms exponential backoff: সবচেয়ে খারাপ ক্ষেত্রে 3 × 300 + 100 + 200 = 1200 ms. স্তর বাড়লে গুণ হয়: তিন স্তরের প্রতিটি ৩ বার চেষ্টা করলে একটি ক্লিকে নিচের সার্ভিসে 3 × 3 × 3 = 27টি অনুরোধ। এই retry storm ঠেকাতে সাধারণত একটি স্তরেই retry রাখা হয়, সাথে jitter. গভীরে পড়তে System Design কোর্সের resilience পর্ব দেখুন, যেখানে circuit breaker-ও আছে। আর queue-র retry পর্ব ৬২-তে।
৮ · শেষ পাহারাদার আর graceful shutdown
কেন্দ্রীয় handler শুধু request-এর পথের ভুল ধরে। তার বাইরের ভুল সোজা process-এর কাছে যায়। Node দুটি event দেয়। uncaughtException চলে কোনো catch না পাওয়া throw-তে। unhandledRejection চলে যে promise-এর কোনো handler নেই তাতে। Node 15 থেকে default হলো: rejection-এর hook না থাকলে সেটি uncaught exception হয়ে ওঠে।
// শেষ পাহারাদার। আসল server-এ এখানে log লিখে process.exit(1)
process.on('uncaughtException', (err, origin) => {
console.log(`[${origin}] ${err.message}`);
});
process.on('rejectionHandled', () => console.log('rejectionHandled: too late'));
process.on('exit', () => console.log('process ended'));
// ১) await ভুলে যাওয়া promise
async function chargeCustomer() {
throw new Error('forgot await on chargeCustomer');
}
chargeCustomer();
// ২) পরে catch জুড়লে দেরি হয়ে যায়
const late = Promise.reject(new Error('catch attached too late'));
setTimeout(() => late.catch(() => console.log('late catch ran')), 30);
// ৩) timer-এর callback: কোনো try/catch-এর ভেতরে নেই
try {
setTimeout(() => { throw new Error('thrown inside timer'); }, 10);
} catch (e) {
console.log('this line never runs');
}
// ৪) ঠিক পথ: এখানেই ধরা হলো, কোনো hook চলবে না
chargeCustomer().catch((e) => console.log('handled here:', e.message));আউটপুট:
handled here: forgot await on chargeCustomer
[unhandledRejection] forgot await on chargeCustomer
[unhandledRejection] catch attached too late
[uncaughtException] thrown inside timer
late catch ran
rejectionHandled: too late
process ended
origin বলে দিচ্ছে ভুলটি কোন পথে এসেছে। try ব্লকটি timer-এর throw ধরতে পারেনি, কারণ throw হওয়ার সময় try অনেক আগেই শেষ। দেরিতে জোড়া catch চলেছে ঠিকই (late catch ran), কিন্তু hook তার আগেই বেজে গেছে, আর Node rejectionHandled দিয়ে শুধু জানিয়েছে যে পরে ধরা হলো। তাই promise বানানোর সাথে সাথেই await বা .catch দিন। এই block শেখানোর জন্য বেঁচে থাকে; server-এ বাঁচিয়ে রাখা চলবে না।
Node-এর ডকুমেন্ট পরিষ্কার: uncaughtException-এর পরে স্বাভাবিক কাজ চালিয়ে যাওয়া নিরাপদ নয়। এর ঠিক ব্যবহার হলো synchronous পরিষ্কার করে process বন্ধ করা। কেন? ভুলটি কোথায় হয়েছিল, কোন lock ধরা ছিল, কোন হিসাব অর্ধেক হয়েছিল, কেউ জানে না। আধখোলা গুদামে বিক্রি চালানোর চেয়ে দোকান বন্ধ করে নতুন করে খোলা ভালো। নতুন করে চালু করার কাজ বাইরের কারও: process supervisor, যেমন systemd, Docker-এর restart policy, Kubernetes বা pm2-এর মতো টুল।
import { app, db } from './app.js';
const PORT = Number(process.env.PORT ?? 3000);
const server = app.listen(PORT, '127.0.0.1');
let closing = false;
function shutdown(signal) {
if (closing) return;
closing = true;
console.log(`${signal}: নতুন অনুরোধ বন্ধ`);
app.locals.ready = false; // health check 503 দেবে
const force = setTimeout(() => {
console.error('সময় শেষ, জোর করে বন্ধ');
process.exit(1);
}, 10_000);
force.unref();
server.close((err) => {
db.close();
console.log('সব অনুরোধ শেষ, DB বন্ধ');
process.exitCode = err ? 1 : 0;
});
// keep-alive সংযোগ: উত্তর শেষে খালি হলেই বন্ধ
setInterval(() => server.closeIdleConnections(), 200).unref();
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('uncaughtException', (err, origin) => {
console.error(JSON.stringify({
level: 'fatal', origin, name: err.name, message: err.message,
}));
process.exit(1); // অবস্থা অজানা: supervisor নতুন করে চালু করবে
});সত্যিকারের run-এ এই পাহারাদার দুটি ভুলই ধরেছে। /api/crash-এর timer-এ throw আর /api/floating-এর ভুলে যাওয়া promise, দুটিতেই exit code 1:
$ curl http://127.0.0.1:47306/api/crash
{"scheduled":true}
[server] {"level":"fatal","origin":"uncaughtException","name":"Error","message":"timer callback bug"}
[server exited code=1 signal=null]
$ curl http://127.0.0.1:47306/api/floating
[server] {"level":"fatal","origin":"unhandledRejection","name":"Error","message":"forgot to await this"}
{"ok":true}
[server exited code=1 signal=null]
দ্বিতীয়টি ভয়ের। Client পেয়েছে {"ok":true}, অথচ যে কাজটি হওয়ার কথা ছিল সেটি ব্যর্থ, আর server নেমে গেছে। “Fire and forget” promise-এর এটাই ঝুঁকি।
Graceful shutdown: দোকান বন্ধের নিয়ম
নতুন version deploy হলে পুরোনো process-কে থামতেই হবে। Orchestrator সাধারণত প্রথমে SIGTERM পাঠায়, কিছুক্ষণ অপেক্ষা করে, তারপর SIGKILL, যেটি ধরা যায় না। ভালো দোকানদার ঠিক এভাবে বন্ধ করেন: শাটার অর্ধেক নামিয়ে নতুন খদ্দের বন্ধ, ভেতরের খদ্দেরের কেনা শেষ, হিসাব মেলানো, তালা। নিচের block-এ সত্যিকারের SIGTERM চলে, process নিজেই নিজেকে পাঠায়।
const http = require('node:http');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const server = http.createServer(async (req, res) => {
console.log('start', req.url);
await sleep(150); // ধীর কাজ, যেমন order লেখা
res.end('done ' + req.url);
console.log('sent ', req.url);
});
let closing = false;
process.on('SIGTERM', () => {
if (closing) return;
closing = true;
console.log('SIGTERM: no new connections');
const force = setTimeout(() => process.exit(1), 500);
force.unref(); // এই timer নিজে process-কে বাঁচিয়ে রাখবে না
server.close(() => {
console.log('all requests finished: close DB, exit');
});
setInterval(() => server.closeIdleConnections(), 50).unref();
});
server.listen(0, '127.0.0.1', async () => {
const url = `http://127.0.0.1:${server.address().port}`;
const order = fetch(url + '/order').then((r) => r.text());
await new Promise((r) => server.once('request', r)); // /order পৌঁছেছে
process.kill(process.pid, 'SIGTERM'); // সত্যিকারের signal, নিজেকেই পাঠানো
await sleep(20);
try {
await fetch(url + '/late');
} catch (e) {
console.log('new request refused:', e.cause.code);
}
console.log('client got:', await order);
});আউটপুট:
start /order
SIGTERM: no new connections
new request refused: ECONNREFUSED
sent /order
all requests finished: close DB, exit
client got: done /order
/order SIGTERM-এর আগে শুরু হয়েছিল, তাই শেষ হলো। /late পরে এসেছে, তাই ECONNREFUSED. একটি অনুরোধও মাঝপথে কাটা পড়েনি। শেষ দুই লাইনের ক্রম কখনো উল্টে যেতে পারে, কারণ server আর client একই process-এ আলাদাভাবে এগোয়। শেষের closeIdleConnections লাইনটি মুছলে এই block-এ force timer চলে আর exit code হয় 1, কারণ fetch-এর keep-alive সংযোগ খোলা থেকে যায়।
একই কাজ আসল Express app-এ, Linux-এ (WSL Ubuntu, Node 24.15.0, সত্যিকারের kill -TERM). /api/slow ১.৫ সেকেন্ড সময় নেয়:
$ curl -s -w " [%{http_code} %{time_total}s]\n" localhost:47331/api/slow &
$ kill -TERM <server pid>
SIGTERM: নতুন অনুরোধ বন্ধ
$ curl -sS localhost:47331/health
curl: (7) Failed to connect to 127.0.0.1 port 47331 after 0 ms: Couldn't connect to server
{"done":true} [200 1.501047s]
সব অনুরোধ শেষ, DB বন্ধ
server exit code: 0
- চলছে
in-flight 1খদ্দের ভেতরে - SIGTERM
ready=falseনোটিশ এলো - close()
নতুন: নাশাটার নামল - Drain
in-flight 0কেনা শেষ - Exit 0
db.close()তালা
| চলমান অনুরোধ | |
| নতুন সংযোগ | |
| force timer | |
| exit code |
- সাধারণ অবস্থা। একটি ধীর অনুরোধ চলছে। Server জানে না এখনই deploy শুরু হবে।
- SIGTERM এলো। Handler চলল।
closingসত্য হলো, যাতে দুবার signal এলেও একবারই চলে।readyমিথ্যা হলো, আর ১০ সেকেন্ডের force timer শুরু হলো। - শাটার নামল।
server.closeনতুন সংযোগ নেওয়া বন্ধ করল। এই ধাপে curl পেয়েছেFailed to connect. চলমান অনুরোধ ছোঁয়া হয়নি। - ভেতরের কাজ শেষ।
/api/slow১.৫০ সেকেন্ডে 200 নিয়ে ফিরল। Keep-alive সংযোগ খালি হতেই বন্ধ হলো। - তালা।
close-এর callback চলল, DB বন্ধ হলো, exit code 0.force.unref()থাকায় timer-এর জন্য process অপেক্ষা করেনি। - অন্য শাখা: কিছু আটকে থাকলে। সময়সীমা ৭০০ ms করে একই পরীক্ষায় ১.৫ সেকেন্ডের কাজ শেষ হয়নি। লাইন এলো
সময় শেষ, জোর করে বন্ধ, exit code 1, আর curl পেল(52), মানে কোনো উত্তর নেই। ছোট ক্ষতি মেনে নিয়ে process থামল, চিরকাল আটকে থাকেনি।
Keep-alive-এর ফাঁদ: server.close ডাকার সময়ে যে সংযোগ ব্যস্ত, তা উত্তরের পরে খোলা থাকতে পারে। fetch দিয়ে একই পরীক্ষায় closeIdleConnections-এর লাইন ছাড়া server থেমেছে ৪,৫১৪ ms-এ, লাইনটিসহ ১,৫০৫ ms-এ (এক local run, সাধারণ ফল নয়)। Windows-এর ফাঁদ: আমাদের Windows run-এ process.kill(pid, 'SIGTERM') handler না চালিয়েই process শেষ করে দিয়েছে, তাই শেখার জন্য Linux বা Docker-এ পরীক্ষা করুন। Load balancer: এখানে client সরাসরি server-এ ছিল। Load balancer থাকলে সে আগে health check-এ ready: false দেখবে; তাই বাস্তবে server.close-এর আগে কয়েক সেকেন্ড অপেক্ষা রাখা হয়, নইলে উপরের কোডে সংযোগই বন্ধ হয়ে যায় আর 503 দেখা যায় না। Health check পর্ব ৩১-এ, আর Kubernetes pod-এর জীবনচক্র Cloud কোর্সে।
আরও গভীরে: process.exit() stdout-এর অসমাপ্ত লেখা কেটে দিতে পারে, তাই Node-এর ডকুমেন্ট স্বাভাবিক পথে process.exitCode ঠিক করে event loop খালি হতে দিতে বলে; আমাদের server.close-এর callback তাই করছে। Stream-এ error (যেমন pipeline) আর EventEmitter-এর 'error' event আলাদা পথ: listener না থাকলে 'error' event throw হয়ে process নামিয়ে দেয়। Node-এর AsyncLocalStorage দিয়ে একটি অনুরোধের পুরো async পথে requestId বয়ে নেওয়া যায়, যা পর্ব ৩১-এর বিষয়। Go বা Rust-এ error একটি সাধারণ মান (Result), আর JavaScript-এও neverthrow-এর মতো library সেই ধরন আনে; সেখানে ভুল type-এ দেখা যায়, দাম হলো প্রতিটি ধাপে হাতে পাঠানো। সম্প্রদায়ে এ নিয়ে একমত নেই।
৯ · Client-এ ফেরা: error থেকে UI
স্থির আকারের আসল লাভ পাওয়া যায় frontend-এ। সব fetch একটি ছোট ফাংশনের ভেতর দিয়ে গেলে React-এর কোডে একটি মাত্র error ধরন থাকে।
export class ApiError extends Error {
constructor(status, body, retryAfter) {
super(body?.error?.message ?? 'সংযোগে সমস্যা হয়েছে');
this.status = status;
this.code = body?.error?.code ?? 'UNKNOWN';
this.fields = body?.error?.fields ?? [];
this.requestId = body?.error?.requestId;
this.retryAfter = retryAfter;
}
}
export async function api(path, init) {
const res = await fetch(path, init);
const isJson = res.headers.get('content-type')?.includes('json');
const body = isJson ? await res.json() : null;
if (!res.ok) {
throw new ApiError(res.status, body, res.headers.get('retry-after'));
}
return body;
}isJson যাচাই কেন? Reverse proxy বা load balancer নিজের HTML পাতা পাঠাতে পারে, যেমন 502-তে। তখন res.json() নিজেই ভেঙে যেত, আর আসল status হারিয়ে যেত। এই ফাংশন lab-এর server-এর সাথে Node 24-এ চালিয়ে দেখা হয়েছে: /api/products/999-এ এসেছে 404 PRODUCT_NOT_FOUND, /health-এ সফল উত্তর। প্রতিটি code-এর জন্য UI আলাদা:
| Server-এর উত্তর | UI-তে যা করবেন | কোথায় শিখেছেন |
|---|---|---|
422, fields | setError দিয়ে ঠিক ঘরে বার্তা | পর্ব ২১ |
409 DUPLICATE_SKU | ঘরে বার্তা, অথবা বিদ্যমান পণ্যের লিঙ্ক | পর্ব ২১ |
404 PRODUCT_NOT_FOUND | “পণ্য নেই” পাতা, retry নয় | পর্ব ১৯ |
| 503, 504 | “আবার চেষ্টা করুন” বোতাম; GET-এ সীমিত retry | পর্ব ১৯ |
| Optimistic update-এর পরে ভুল | আগের অবস্থায় rollback, toast-এ বার্তা | পর্ব ১৯ |
500 INTERNAL | সাধারণ বার্তা আর requestId, যাতে ব্যবহারকারী জানাতে পারেন | এই পর্ব |
| Render-এর ভুল | Error boundary-এর বিকল্প UI | পর্ব ২৩ |
useQuery({
queryKey: ['product', id],
queryFn: () => api(`/api/products/${id}`),
retry: (count, err) => err.status >= 500 && count < 2, // 404-এ retry নয়
});
const save = useMutation({
mutationFn: (data) => api('/api/products', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(data),
}),
onError: (err) => {
err.fields.forEach((f, i) => form.setError(f.path, {
type: 'server', message: f.message,
}, { shouldFocus: i === 0 }));
},
});লক্ষ করুন, f.path-এর আকার sku বা items.0.qty, যা React Hook Form সরাসরি বোঝে। Server-এর একটি ছোট সিদ্ধান্ত frontend-এর অনেক কোড বাঁচাল। উপরের React অংশটি উদাহরণ, এখানে চালানো হয়নি; মূল ধরন পর্ব ১৯ আর পর্ব ২১-এ চালিয়ে দেখানো আছে।
পরীক্ষা ও ইন্টারভিউতে: Operational error আর bug-এর পার্থক্য? প্রথমটি প্রত্যাশিত পরিস্থিতি, দ্বিতীয়টি কোডের ভুল। Express error middleware কীভাবে চেনে? চারটি argument দেখে, আর এটি সব route-এর পরে বসে। Express 5-এ কি সব async error ধরা পড়ে? না, শুধু handler-এর ফেরত দেওয়া promise-এর। Uncaught exception-এর পরে কি চালিয়ে যাওয়া উচিত? না, log লিখে exit করে supervisor-কে নতুন process চালাতে দিন। SIGTERM পেলে server কী করে? নতুন সংযোগ বন্ধ করে, চলমান কাজ শেষ করে, resource বন্ধ করে exit করে, আর একটি সর্বোচ্চ সময়সীমা রাখে।
১০ · AI-এর যুগে: error handling যাচাই
AI দিয়ে লেখা server কোড সুন্দর দেখায়, সুখের পথেও চলে। সমস্যা লুকিয়ে থাকে ভুলের পথে, যেটি demo-তে কেউ চালায় না। তাই ইচ্ছা করে ভুল ঘটিয়ে দেখা ছাড়া এই কোড বিশ্বাস করা যায় না।
| AI-এর ভুল | কেন ক্ষতিকর | যাচাই |
|---|---|---|
Express 5-এ express-async-errors বা asyncHandler বসানো | অদরকারি dependency, পুরোনো ধরনের কোড | package.json-এর version দেখুন |
res.status(err.status).json(...) | status না থাকলে নিজেই ভাঙে: সত্যিকারের run-এ TypeError: Invalid status code: undefined | null.x ধরনের bug ঘটিয়ে দেখুন |
message: err.message সবসময় পাঠানো | SQL আর ভেতরের ঠিকানা বাইরে যায় | SQL-এ বানান ভুল করে উত্তর পড়ুন |
সব catch-এ console.log করে থেমে যাওয়া | Error লুকিয়ে যায়, request ঝোলে | Timeout দিয়ে curl চালান |
uncaughtException-এ শুধু log, exit নেই | নষ্ট অবস্থায় process চলতে থাকে | Timer-এ throw করে exit code দেখুন |
fetch-এ কোনো timeout নেই | বাইরের সমস্যা আপনার সমস্যা হয়ে যায় | ধীর নকল সার্ভিস দিয়ে চালান |
| POST-এ অন্ধ retry | দুবার order বা দুবার টাকা কাটা | Idempotency key আছে কি না দেখুন |
AI-কে “error handling যোগ করো” না বলে ভুলের তালিকা দিন, আর প্রমাণ চান। লক্ষ করুন, শেষে পরীক্ষার তালিকাও চাওয়া হয়েছে:
AI-এর লেখা error handling হাতে পেলে
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১Operational error আর programmer bug ভাগ করার চিন্তা কোথা থেকে এলো, আর এই ভাগ কি সবসময় পরিষ্কার?
Node.js জগতে এই ভাগকে জনপ্রিয় করেছে Joyent-এর একটি লেখা, “Error Handling in Node.js”, যা Dave Pacheco ২০১৪ সালে লিখেছিলেন বলে জানা যায়। সেই সময়ে Node-এ callback-ই ছিল মূল পথ, আর সবাই এক প্রশ্নে আটকে যেতেন: ভুল হলে throw করব, callback-এ দেব, নাকি event ছাড়ব? লেখাটির যুক্তি ছিল, আগে ঠিক করুন ভুলটি কোন জাতের। ব্যর্থ সংযোগ বা ভুল ইনপুট সঠিক প্রোগ্রামেও ঘটে, তাই এগুলোর পরিকল্পিত উত্তর থাকা উচিত। আর undefined-এর property পড়া মানে প্রোগ্রাম নিজেই ভুল, তাই রানটাইমে এর “সমাধান” হয় না, সমাধান হয় কোড বদলে।
তবে সীমারেখা সবসময় পরিষ্কার নয়। প্রথম উদাহরণ: database সংযোগ হারানো সাধারণত operational, কিন্তু ভুল password দিয়ে deploy করলে সেটি configuration-এর ভুল, যা নিজে ঠিক হবে না। দ্বিতীয় উদাহরণ: একটি library ভেতরে TypeError ছুড়লে আপনার কোড ভুল ইনপুট দিয়েছে, নাকি library-তে bug? তৃতীয় উদাহরণ: পেমেন্ট সার্ভিসের timeout operational হলেও দিনে হাজারবার ঘটলে সেটি কারও জাগিয়ে তোলার মতো ঘটনা। তাই log level ঠিক হয় শুধু “কোন জাত” দেখে নয়, “কত ঘন ঘন, কত ক্ষতি” দেখেও। বাজারঘরের কোডেও timeout-কে error level দেওয়া হয়েছে।
একটি বাস্তব কৌশল হলো ধরে নেওয়া: যা চিনি না, সেটি bug. toAppError ঠিক তাই করছে। অচেনা মানেই 500 আর পুরো log. এতে ভুল ভাগে পড়লেও ক্ষতি ছোট থাকে: বড়জোর একটি অদরকারি alert, কিন্তু তথ্য ফাঁস নয়। পরে সেই ভুল চিনে তালিকায় যোগ করলেই হলো।
মূল উপলব্ধি: ভাগটি প্রকৃতির নিয়ম নয়, একটি প্রকৌশল সিদ্ধান্ত। আপনি যা আগে ভেবে রেখেছেন সেটিই operational, বাকি সব অজানা। অজানাকে সবসময় সবচেয়ে সাবধানী পথে পাঠান।
প্র ০২একটি ভুলে পুরো server থামিয়ে দেওয়া কি বাড়াবাড়ি নয়? বাকি ব্যবহারকারীদের কী দোষ?
শুনতে উল্টো লাগে, কিন্তু এর পেছনে দীর্ঘ প্রকৌশল-অভিজ্ঞতা আছে। Ericsson-এ টেলিফোন সুইচের জন্য তৈরি Erlang ভাষার জগতে একটি দর্শন বিখ্যাত হয়েছে: “let it crash”. Joe Armstrong ও তাঁর সহকর্মীদের চিন্তা ছিল, প্রতিটি সম্ভাব্য ভুলের জন্য প্রতিরক্ষামূলক কোড লেখা অসম্ভব। তার চেয়ে ছোট ছোট process বানান, ভুল হলে একটি মরুক, আর একজন supervisor সেটিকে পরিচিত পরিষ্কার অবস্থা থেকে আবার চালু করুক। লক্ষ করুন, Erlang-এর process খুব হালকা আর একে অপরের memory ভাগ করে না। তাই একটির মৃত্যুতে বাকিরা অক্ষত থাকে।
Node-এর একটি process-এ কিন্তু হাজার অনুরোধ একই memory ভাগ করে। তাই প্রশ্নটি আসে। উত্তর হলো, চালিয়ে গেলে ক্ষতি আরও লুকানো হতে পারে। ধরুন একটি stock কমানোর কাজ মাঝপথে throw হলো। একটি transaction খোলা রয়ে গেল, একটি lock ধরা থাকল, একটি cache-এ অর্ধেক লেখা মান রইল। এরপর হাজার অনুরোধ সেই নষ্ট অবস্থার ওপর কাজ করবে, হয়তো ভুল হিসাব লিখবে। Crash হলে সমস্যা শব্দ করে; নীরব তথ্য-নষ্ট হলে কয়েক সপ্তাহ পরে হিসাব মেলে না।
তাই “থামাব, না চালাব” এই একটি প্রশ্ন নয়। আগে ক্ষতি কমান: একাধিক process চালান (cluster বা একাধিক container), load balancer রাখুন, supervisor রাখুন। তখন একটি process থামলে অল্প কয়েকটি অনুরোধই ক্ষতিগ্রস্ত হয়। আর এই ঘটনা থেকে bug ধরা পড়ে, যা ঠিক করলে আর ঘটবে না। ধরুন একটি অনুরোধ দিয়ে যে কেউ বারবার crash ঘটাতে পারে; তখন এটি denial of service, আর সমাধান সেই bug ঠিক করা, hook দিয়ে চাপা দেওয়া নয়। কিছু দল exit-এর আগে চলমান অনুরোধ অল্প সময় শেষ করতে দেয়; এটি যুক্তিসঙ্গত আপস, তবে নতুন অনুরোধ নেওয়া বন্ধ রেখে আর কঠিন সময়সীমায়।
মূল উপলব্ধি: process-কে সস্তা আর বদলযোগ্য ধরে নকশা করুন। তখন থামা আর দুর্ঘটনা থাকে না, হয়ে যায় নিরাপত্তার একটি ব্যবস্থা।
প্র ০৩Node 15-এর আগে unhandled rejection শুধু warning ছিল। Node কেন এমন ভাঙনধর্মী পরিবর্তন আনল?
Promise যখন Node-এ এলো, তখন একটি সূক্ষ্ম প্রশ্ন ছিল: rejected promise-এর handler নেই মানেই কি ভুল? হয়তো handler একটু পরে জুড়বে। তাই দীর্ঘদিন Node শুধু console-এ একটি warning দেখাত। সেই warning-এ বলা থাকত ভবিষ্যতে এমন হলে process বন্ধ হবে। কিন্তু warning কেউ পড়ত না। Log-এ লাইনটি জমত, আর আসল ভুল অদৃশ্য থাকত। Express ৪-এর async handler-এ ঠিক তাই হতো: request ঝুলে থাকত, process চলত, কেউ কারণ খুঁজে পেত না।
২০২০ সালের Node 15.0.0-তে --unhandled-rejections-এর default warn থেকে বদলে throw হলো। Changelog অনুযায়ী এখন hook না থাকলে unhandled rejection-কে uncaught exception হিসেবে তোলা হয়। যুক্তিটি ছিল সামঞ্জস্য: synchronous throw যদি process থামায়, তবে async throw কেন নীরব থাকবে? এর ফলে অনেক পুরোনো app আপগ্রেডের পরে হঠাৎ crash করতে শুরু করে। আসলে ভুলগুলো আগেও ছিল, শুধু এখন দেখা যাচ্ছিল। ফেরার পথও রাখা হয়েছিল --unhandled-rejections=warn flag দিয়ে, তবে সেটি স্থায়ী সমাধান নয়, কারণ এতে ভুল আবার লুকিয়ে যায়। সঠিক পথ ছিল log দেখে প্রতিটি ভুলে যাওয়া await খুঁজে ঠিক করা।
এই পর্বের দুটি সত্যিকারের ফলের সাথে মিলিয়ে দেখুন। Express ৪-এ /async process নামিয়েছে, আর hook বসালে request ঝুলেছে, দুটিই সেই ইতিহাসের ফল। আপনার যদি পুরোনো Express ৪ app থাকে আর সেখানে Node আপগ্রেড হয়, তবে এই পরিবর্তন সরাসরি আপনার গায়ে লাগবে। Express ৫ সেই ফাঁক অনেকটা বন্ধ করেছে handler-এর স্তরে, কিন্তু /floating-এর মতো ফেরত না দেওয়া promise-এর দায় আপনারই। TypeScript-এর no-floating-promises lint নিয়ম (typescript-eslint) এই ভুল লেখার সময়েই ধরতে সাহায্য করে।
মূল উপলব্ধি: নীরব ব্যর্থতা জোরে ব্যর্থতার চেয়ে খারাপ। কখনো ভালো default-এর দাম দিতে হয় পুরোনো কোড ভেঙে, কিন্তু তাতে লুকানো bug সামনে আসে।
প্র ০৪Error বার্তা কি সত্যিই নিরাপত্তার বিষয়? মানুষ তো সাহায্যের জন্যই বিস্তারিত বার্তা চান।
হ্যাঁ, আর মানদণ্ডগুলোও তাই বলে। MITRE-এর দুর্বলতার তালিকায় CWE-209 নামে একটি শ্রেণি আছে: এমন error বার্তা তৈরি করা, যাতে সংবেদনশীল তথ্য থাকে। OWASP Top 10-এর ২০২৫ সংস্করণে A10: Mishandling of Exceptional Conditions নামে একটি নতুন বিভাগ এসেছে, যা ব্যতিক্রমী পরিস্থিতি ঠিকমতো না সামলানোকে আলাদা ঝুঁকি হিসেবে তুলে ধরে। RFC 9457-ও নিজের নিরাপত্তা অংশে সাবধান করে যে stack dump-এর মতো ভেতরের তথ্য HTTP দিয়ে প্রকাশ করা এড়াতে হবে।
আক্রমণ সাধারণত একবারে হয় না, হয় ধাপে ধাপে। আক্রমণকারী ইচ্ছা করে ভাঙা ইনপুট পাঠান, উত্তর পড়েন, আবার পাঠান। SQL error-এ টেবিলের নাম পেলে পরের injection অনেক সহজ। Stack trace-এ framework আর ফাইলের গঠন পেলে জানা দুর্বলতা খোঁজা সহজ। আবার শুধু বার্তার পার্থক্যই তথ্য দেয়: “ইমেইল নেই” আর “password ভুল” আলাদা হলে কোন ইমেইলে অ্যাকাউন্ট আছে জানা যায়, যাকে user enumeration বলে। সময়ের পার্থক্যও একই তথ্য দিতে পারে, কারণ password যাচাই না হলে উত্তর দ্রুত আসে। তাই নিরাপদ বার্তা লেখা শুধু লেখার কাজ নয়, নকশার কাজ।
তাহলে কি বার্তা অস্পষ্ট রাখব? না। দ্বন্দ্বটি মিথ্যা। সাহায্য দরকার দুই জনের, আর দুজনের তথ্য আলাদা। ব্যবহারকারী জানতে চান কী বদলাবেন: “মোবাইল নম্বর ১১ অঙ্কের হবে”। এটি নিরাপদ, কারণ তাঁর নিজের ইনপুট নিয়ে কথা। ডেভেলপার জানতে চান কোন লাইনে কী হলো; সেটি log-এ থাকে। আর দুজনকে জোড়ে requestId: ব্যবহারকারী “কোড 34d3376d” জানালেই সহায়তা দল পুরো ঘটনা পেয়ে যায়। 4xx বিস্তারিত, 5xx সংক্ষিপ্ত, এটিই সাধারণ চর্চা। গভীরে যেতে API security পর্ব দেখুন।
মূল উপলব্ধি: ভালো error বার্তা তথ্য কমায় না, তথ্য সঠিক মানুষের কাছে পাঠায়।
প্র ০৫Retry তো ভালো জিনিস। তাহলে retry কেন বড় outage-কে আরও বড় করে?
একটি নিচের সার্ভিস অতিরিক্ত চাপে ধীর হলো। প্রতিটি উপরের সার্ভিস timeout পেয়ে আবার চেষ্টা করল। এখন একই সার্ভিসে অনুরোধ দ্বিগুণ বা তিনগুণ, ঠিক যখন তার ক্ষমতা সবচেয়ে কম। সে আরও ধীর হলো, আরও timeout, আরও retry. এই চক্রকে Google-এর Site Reliability Engineering বইয়ের cascading failure অধ্যায়ে বিস্তারিত আলোচনা করা হয়েছে, আর Amazon-এর Builders' Library-র “Timeouts, retries, and backoff with jitter” লেখাতেও একই সতর্কতা আছে। সার্ভিসটি যদি ফিরেও আসে, জমে থাকা চাপ তাকে আবার নামিয়ে দিতে পারে।
এই পর্বের হিসাবে ফিরে যাই। তিন স্তর, প্রতিটিতে ৩ বার চেষ্টা, মানে একটি ক্লিকে ২৭টি অনুরোধ। সেকেন্ডে ৫০ ক্লিক হলে নিচের সার্ভিসে সবচেয়ে খারাপ ক্ষেত্রে সেকেন্ডে ১,৩৫০টি অনুরোধ। স্বাভাবিকের ২৭ গুণ চাপ আসে ঠিক দুর্বল মুহূর্তে।
প্রতিকার কয়েকটি। একটি স্তরে retry: সাধারণত সবচেয়ে ওপরে বা সবচেয়ে নিচে, সব জায়গায় নয়। Exponential backoff আর jitter: অপেক্ষা বাড়ান আর এলোমেলো করুন, যাতে সবাই একই মুহূর্তে ফিরে না আসে। Retry budget: মোট অনুরোধের একটি ছোট অংশের বেশি retry নয়। Circuit breaker: অনেক ব্যর্থতার পরে কিছুক্ষণ চেষ্টাই বন্ধ, দ্রুত ভুল ফেরত। ঢাকার যানজটের সাথে মিলিয়ে ভাবুন: মোড় বন্ধ দেখে সবাই একসাথে একই গলিতে ঢুকলে গলিটিও বন্ধ হয়। Deadline পাঠানো: client-এর ২ সেকেন্ড শেষ হলে নিচের কাজ চালিয়ে লাভ নেই। React-এর দিকেও একই নিয়ম: TanStack Query-র default retry 4xx-এ বন্ধ করা উচিত, যা পর্ব ১৯-এ দেখেছেন।
আর Retry-After header এখানে কাজে আসে, কারণ server নিজেই বলে দেয় কখন ফেরা যুক্তিসঙ্গত। 429 আর 503-তে এটি বিশেষ উপকারী।
মূল উপলব্ধি: Retry হলো একই সার্ভিসে বাড়তি চাপ পাঠানো। সীমা ছাড়া এটি সাহায্য নয়, নিজের তৈরি আক্রমণ।
অনুশীলন
প্রথমে নিজে চেষ্টা করুন, তারপর উত্তর মিলিয়ে নিন।
-
সংখ্যার হিসাব: চেকআউটের মোট সময়সীমা ২.৫ সেকেন্ড। Gateway-এর timeout ৮০০ ms, মোট ৩ বার চেষ্টা, মাঝে ২০০ আর ৪০০ ms অপেক্ষা। (ক) সবচেয়ে খারাপ ক্ষেত্রে মোট সময় কত? (খ) সীমার ভেতরে আসতে timeout কত করবেন? (গ) সেকেন্ডে ৪০টি অনুরোধে gateway আটকে গেলে ৬০০ ms timeout-এ কতগুলো অনুরোধ একসাথে অপেক্ষায় থাকবে (retry বাদে)?
(ক) 3 × 800 + 200 + 400 = 3000 ms, যা ২,৫০০ ms-এর বেশি।
(খ) অপেক্ষা বাদে হাতে থাকে 2500 − 600 = 1900 ms. 1900 ÷ 3 ≈ 633 ms. তাই গোল সংখ্যায় ৬০০ ms: মোট 3 × 600 + 600 = 2400 ms, সীমার ভেতরে। আরেক পথ হলো চেষ্টা ২ বারে নামানো: 2 × 800 + 200 = 1800 ms.
(গ) L = λ × W = 40 × 0.6 = 24টি।
-
আউটপুট অনুমান: Express 5-এ
GET /xএলে console-এ কী কী ছাপা হবে, ক্রমসহ? Client কী পাবে? (AppError-এstatusওcodeআছে।)app.use((req, res, next) => { console.log('A'); next(); }); app.get('/x', async () => { console.log('B'); throw new AppError(409, 'DUP', 'already exists'); }); app.use((req, res, next) => { console.log('C'); next(); }); app.use((err, req, res, next) => { console.log('D', err.status); next(err); }); app.use((err, req, res, next) => { console.log('E'); res.status(err.status).json({ code: err.code }); });সত্যিকারের run (Express 5.3.0):
A B D 409 E HTTP/1.1 409 Conflict {"code":"DUP"}Cছাপা হয়নি, কারণ সারি error পথে যাওয়ার পরে তিন argument-এর middleware বাদ পড়ে।Dnext(err)দিয়ে ভুলটি পরের error middleware-এ পাঠিয়েছে।next()লিখলে সারি আবার স্বাভাবিক পথে ফিরত, আরEচলত না। -
Code review: AI নিচের কোড দিয়েছে। অন্তত পাঁচটি সমস্যা বের করুন, আর ঠিক করার পথ বলুন।
app.use((err, req, res) => { res.status(err.status).json({ message: err.message, stack: err.stack, }); }); app.get('/api/orders/:id', async (req, res) => { /* … */ }); process.on('uncaughtException', (e) => console.log(e));- তিন argument: Express এটিকে error middleware হিসেবেই চেনে না।
nextযোগ করতে হবে। - Route-এর আগে বসানো: Handler সব route-এর পরে থাকতে হবে।
err.statusনাও থাকতে পারে: Bug-এundefinedআসে, আর Express 5TypeError: Invalid status code: undefinedছোড়ে।?? 500বাtoAppErrorলাগবে।- ফাঁস:
err.messageআরerr.stackclient-এ যাচ্ছে। শুধু log-এ রাখতে হবে। uncaughtException-এর পরে exit নেই: Process নষ্ট অবস্থায় চলবে। Log লিখেprocess.exit(1).- বাড়তি:
res.headersSentযাচাই নেই, 404 catch-all নেই,codeআরrequestIdনেই, log-এর আকার এলোমেলো।
- তিন argument: Express এটিকে error middleware হিসেবেই চেনে না।
-
Shutdown-এর সময়ের নকশা: ধরুন platform SIGTERM-এর ৩০ সেকেন্ড পরে SIGKILL পাঠায় (ধরে নেওয়া মান; নিজের platform-এ দেখে নিন)। সবচেয়ে ধীর অনুরোধ ১২ সেকেন্ড, আর load balancer সরিয়ে নিতে ৫ সেকেন্ড লাগে। ক্রমটি লিখুন, আর force timer কত রাখবেন বলুন।
ক্রম: SIGTERM পেয়ে
ready=false→ ৫ সেকেন্ড অপেক্ষা, যাতে load balancer সরে যায় →server.close→ সর্বোচ্চ ১২ সেকেন্ডে চলমান কাজ শেষ → DB বন্ধ → exit 0. দরকারি সময় 5 + 12 = 17 s.Force timer অন্তত ১৭ সেকেন্ড, আর ৩০-এর চেয়ে কম, যাতে নিজের log লিখে নিজে থামে। যেমন ২৫ সেকেন্ড। এতে ৫ সেকেন্ড হাতে থাকে। SIGKILL এলে log লেখার সুযোগও মিলত না।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমExpress 5-এ একটি async handler-এর throw থেকে উত্তর পর্যন্ত ধাপগুলো সাজান (▲▼ চেপে সরান)
- Handler-এ
throw new AppError(...) - Async ফাংশনের ফেরত দেওয়া promise reject হয়
- Router
next(err)ডাকে - তিন argument-এর middleware বাদ পড়ে
- চার argument-এর
errorHandlerচলে - Log লেখা হয়, client পায় JSON উত্তর
নিচের কোনটি programmer bug, operational error নয়?
- ব্যবহারকারী এমন পণ্য চাইলেন, যা নেই
- Handler-এ
null-এর property পড়তে গিয়েTypeError - পেমেন্ট সার্ভিস ৩০০ ms-এ সাড়া দিল না
- ফর্মে মূল্য ঋণাত্মক পাঠানো হলো
Express 5-এ নিচের কোন ভুল error middleware-এ নিজে থেকে পৌঁছায় না?
- Handler-এ সরাসরি
throw asynchandler-এawait-এর পরেthrow- Handler-এর ভেতরে
setTimeout-এর callback-এthrow - Handler থেকে
return Promise.reject()
app.use((err, req, res) => { … }) লেখা হলো সব route-এর পরে। Throw হলে কী ঘটবে?
- এই ফাংশনটি চলবে, উত্তর যাবে
- Server চালু হওয়ার সময়ই warning দেখাবে
- Process crash করবে
- ফাংশনটি বাদ পড়বে, Express-এর default handler উত্তর দেবে
NODE_ENV সেট না থাকা অবস্থায় Express-এর default error handler কী করে?
- উত্তরের body-তে stack trace পাঠায়
- শুধু
Internal Server Errorলেখা পাঠায় - JSON-এ
codeআরmessageপাঠায় - কোনো উত্তর না দিয়ে request ঝুলিয়ে রাখে
Timeout ৩০০ ms, মোট ৩ বার চেষ্টা, মাঝে ১০০ আর ২০০ ms অপেক্ষা। সবচেয়ে খারাপ ক্ষেত্রে মোট সময় কত?
- 900 ms
- 1000 ms
- 1200 ms
- 1500 ms
Node.js server SIGTERM পেলে সবচেয়ে ভালো আচরণ কোনটি?
- সাথে সাথে
process.exit(0) - নতুন সংযোগ বন্ধ করে চলমান কাজ শেষ করা, resource বন্ধ করা, সময়সীমা রাখা
- Signal উপেক্ষা করে কাজ চালিয়ে যাওয়া
- চলমান কাজ শেষ করা, তবে নতুন অনুরোধও নিতে থাকা
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Operational error-এর উত্তর পরিষ্কার বার্তা, bug-এর উত্তর সাধারণ 500 আর সংশোধন। অচেনা মানেই bug.
- Express 5 শুধু ফেরত দেওয়া promise ধরে। Timer, callback আর ভুলে যাওয়া promise আপনার দায়িত্ব।
- শেষে
notFound, তারপর চার argument-এরerrorHandler; একটি JSON আকার, স্থিরcode,requestId. - Stack, SQL আর path থাকে log-এ, client-এ নয়; stack দিন শুধু
NODE_ENV === 'development'হলে। - প্রতিটি বাইরের call-এ timeout; uncaught ভুলে log লিখে exit; SIGTERM-এ graceful shutdown, সময়সীমাসহ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৯ · Backend-এর গঠন — controller, service, repository আর dependency injectionপরবর্তী পর্বভুল কোন স্তরে ধরা হবে আর কোন স্তরে ছোড়া হবে।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপুরো কোর্সের পথ।
- JavaScript · Error handlingভিত্তিtry/catch, throw আর Error object-এর মূল ধারণা।
- System Design · Resilience patternsগভীরে যানTimeout, retry, circuit breaker আর bulkhead.
- Cybersecurity · API securityনিরাপত্তাAPI-র ঝুঁকি আর তথ্য ফাঁস ঠেকানো।