همونطور که در مقاله قبل گفتیم بوجود اومدن خطا و error handling(کنترل و حل کردن) اونها ممکنه زمان بر باشه و بدتر از اون رو اعصابمون بره، چون انتظارشونو نداشتیم … و خب با نوشتن کد خوب میتونیم از خیلی از اونها جلوگیری کنیم. توی این مقاله از کد اکسپلور میخوایم تعدادی از اونها رو ببینم(که همه انواعی از system error ها هستن) و راههایی برای پیشگیری از ارورها رو هم بگیم. هر قسمتی که نیاز بود بیشتر خونده بشه یا مبحث یکم سنگین شد یا طولانی، سعی می کنم به منابع بیرون ارجاع بدم. باهامون همراه باشید 😉
Heap Out of Memory
این خطا زمانی رخ میده که برنامه از محدودیت حافظه (memory limit) تعیینشده برای فضای Heap در موتور V8 JavaScript (یا سایر موتورهای جاوااسکریپت) فراتر میره. به چند دلیل ممکنه این اتفاق بیوفته:
- هدر رفتن حافظه(memory leaks): که معمولاً به دلیل انباشته شدن اشیاء استفاده نشده یا ارجاع نشده توی حافظه، بدون رهاسازی مناسب اتفاق میافته(البته درمورد Heap Out of Memory صحبت میکنیم الان نه memory leaks).
- خوندن dataset های بزرگ: مدیریت حجم خیلی زیادی از داده توی حافظه بطور همزمان و بدون یه مدیریت خوب میتونه باعث همچین مشکلی بشه.
- محدودیت پیشفرض حافظه: خب node.js یک محدودیت پیشفرض داره که ممکنه(صرفا ممکنه) برای همه برنامهها کافی نباشه و میشه تغییرش داد.
برای حل کردن این مشکل باید برنامه رو بررسی کنیم و جلوی هدر رفتن حافظه رو بگیریم، و برنامه رو برای کارآمدی بیشتر حافظه بهینهتر کنیم. یا میتونیم محدودیت حافظه heap رو بیشتر کنیم.
موقع استفاده از dataset های بزرگ نباید همه چیز رو توی حافظه بخونید. باید از stream های node.js استفاده کنیم تا اونها رو بصورت قطعه قطعه پردازش کنیم.
ECONNRESET
این خطا زمانی پیش میاد که یک اتصال TCP بطور غیر منتظرهای توسط کلاینت(client) یا سرور بسته بشه. مثلا ممکنه به این دلیل پیش بیاد که شما یه درخواست(request) خارجی میزنید ولی جواب(response) اونو به موقع نمیگیرید، یا زمانی که میخواید به درخواست کلاینت جواب بدید ولی اتصال قبل اون بسته شده.
این مشکل رو ممکنه به شکلهای مختلفی ببینید، مثلا:
Error: socket hang up at connResetException (node:internal/errors:691:14) at Socket.socketOnEnd (node:_http_client:466:23) at Socket.emit (node:events:532:35) at endReadableNT (node:internal/streams/readable:1346:12) at processTicksAndRejections (node:internal/process/task_queues:83:21) { code: 'ECONNRESET' }
اگه این مشکل بخاطر درخواست خارجی رخ داد(درخواستی ما میفرستیم) میتونیم error handling رو اینطوری انجام بدیم: درخواست رو دوباره تکرار کنیم و بفرستیم. یا تنظیمات timeout رو برای مدت طولانیتری تنظیم کنیم(توی این مثال درخواستمون رو با کتابخونه axios ارسال کردیم که یه کتابخونه برای انجام درخواستهای http هست):
const response = await axios.get( 'https://example.com', { timeout: 5000, // 5 seconds } );
اگه درخواست از طرف کلاینت بسته شده بود باید اتصال رو قطع کنیم(بوسیله ()res.end و …) و همه فرایندهایی که برای تولید پاسخ(response) درحال انجام هستن رو هم متوقف کنیم.
میتونیم با نظارت روی یه event به اسم close توی درخواست، یا بررسی res.socket.destroyed شناسایی کنیم و error handling رو روش انجام بدیم. نمونش:
app.get("/", (req, res) => { // listen for the 'close' event on the request req.on("close", () => { console.log("closed connection"); }); console.log(res.socket.destroyed); // true if socket is closed });
ENOTFOUND
ENOTFOUND زمانی ایجاد میشه که بدلیل خطای DNS ارتباط با دامنه شکل نگیره. مثلا این قضیه میتونه بدلیل اشتباه نگراشی توی دامنه باشه. راههایی برای حل این مشکل وجود داره مثل:
- مطمئن بشید اون دامنه در منطقه ما در دسترس هست. برای این کار میتونید از ابزارهایی که وجود دارن استفاده کنید.
- اگه از ()http.request یا ()https.request استفاده میکنید دقت کنید که host :property رو دقیقا برابر اسم دامنه یا ip قرار بدین، بدون پورت و مسیر اضافه:
// روش اشتباه const options = { host: 'http://example.com/path/to/resource', }; // روش درست const options = { host: 'example.com', path: '/path/to/resource', }; http.request(options, (res) => {});
- ممکنه برای وصل شدن به localhost هم همچین مشکلی پیش بیاد که میتونید حلش کنید.
ETIMEDOUT
این خطا نشون میده اتصال بدلیل timeout قطع شده. معمولا اگه تنظیمات خاصی برای timeout وجود داشته باشه این مشکل پیش میاد. برای حل این مشکل باید ارور رو قطع کنیم و درخواست رو دوباره بفرستیم. برای این کار میتونیم از روش exponential backoff استفاده کنیم(معادل فارسی خوبی واسش پیدا نکردم :/ ). این روش به تدریج زمان انتظار بین هربار درخواست مجدد رو زیادتر میکنه، و اینجوری شانس موفقیت رو بالا میبره یا به حداکثر محدودیت تلاش مجدد میرسه. بسته(package) fetch-retry میتونه توی این زمینه کمککننده باشه.
ECONNREFUSED
خطای ECONNREFUSED زمانی پیش میاد که node.js سعی میکنه با یک آدرس مشخص ارتباط برقرار کنه(مثلا یه سرویس خاص) اما بدلیل در دسترس نبودن نمیتونه این کارو انجام بده(معمولا بدلیل غیر فعال بودن اون سرویس). برای حل این مشکل باید مطمئن شیم سرویس هدفی که میخوایم بهش متصل شیم فعاله و آماده پذیرش اتصالات.
ERRADDRINUSE
این ارور زمانی رخ میده که سرور بخواد به پورتی متصل شه که توسط یه سرویس دیگه درحال استفاده است. و معمولا زمان راه اندازی یا راه اندازی مجدد سرور پیش میاد.
ECONNABORTED
این مشکل زمانی اتفاق میافته که سرور قبل از اتمام خوندن بدنه درخواست یا نوشتن بدنه پاسخ، اتصال شبکه رو پیش از موعد قطع میکنه. بیاید یه مثال ببینیم که واضح شه:
const express = require('express'); const app = express(); const path = require('path'); app.get('/', function (req, res, next) { res.sendFile(path.join(__dirname, 'new.txt'), null, (err) => { console.log(err); }); res.end(); }); const server = app.listen(3000, () => { console.log('server listening at port 3001......'); });
در اینجا res.sendFile بصورت async اجرا میشه. و به همین دلیل قبل از تموم شدن res.sendFile،تیکه کد ()res.end پاسخ رو تموم میکنه(و اجازه نمیده دیگه چیزی توی پاسخ نوشته بشه). خب این دوتا به هم میخورن، اتصالی میدن و نتیجه این شکلی میشه:
Error: Request aborted at onaborted (/home/ayo/dev/demo/node_modules/express/lib/response.js:1030:15) at Immediate._onImmediate (/home/ayo/dev/demo/node_modules/express/lib/response.js:1072:9) at processImmediate (node:internal/timers:466:21) { code: 'ECONNABORTED' }
برای اینکه از این مشکل جلوگیری کنیم باید مطمئن بشیم ()res.end بعد از تموم شدن فرایند اجرا میشه. مثلا این شکلی(با async و await هم میتونید):
app.get('/', function (req, res, next) { res.sendFile(path.join(__dirname, 'new.txt'), null, (err) => { console.log(err); res.end(); }); });
ENOENT
خطای ENOENT که مخفف “Error No Entity” هست، و نشون میده مسیر مشخص شده برای یه فایل یا دایرکتوری وجود نداره. معمولا زمان استفاده از ماژول fs رخ میده. اینم مثالش:
fs.open('non-existent-file.txt', (err, fd) => { if (err) { console.log(err); } });
ارورش اینطوری نشون داده میشه:
[Error: ENOENT: no such file or directory, open 'non-existent-file.txt'] { errno: -2, code: 'ENOENT', syscall: 'open', path: 'non-existent-file.txt' }
برای error handling این خطا باید مطمئن بشیم فایلی که دنبالشیم وجود داره و مسیوی که دادیم هم درست باشه.
این مشکل میتونه بخاطر مسیرهای ارائه شده توسط کاربر هم پیش بیاد. پس موقع کار با مسیرهایی که کاربر داده باید error handling رو به خوبی انجام بدیم و ارور رو به کاربر نشون بدیم!
این مطلب هم احتمالا براتون جالب باشه: تفاوتهای CommonJs و ESM
EISDIR
این اتفاق زمانی میافته که یه فرایند در node.js یک فایل میخواد، ولی ما به اون یه دایرکتوری میفرستیم :/
// config is a directory fs.readFile('config', (err, data) => { if (err) throw err; console.log(data); }); // output // [Error: EISDIR: illegal operation on a directory, read] { // errno: -21, // code: 'EISDIR', // syscall: 'read' // }
برعکس همین ارور هم هست، به اسم ENOTDIR. وقتی که یه عملیات نیاز به یه دایرکتوری داره ولی یه فایل بهش بدیم.
EEXIST
این مشکل زمانی بوجود میاد که سعی کنیم دایرکتوری رو بسازیم که اسم اون وجود داره. فرض کنید ما temp رو داشته باشیم و این کد رو اجرا کنیم(ماژول fs که برای کار با فایلهاست در این کد استفاده شده):
fs.mkdirSync('temp', (err) => { if (err) throw err; });
که تقریبا با این ارور مواجه میشیم:
Error: EEXIST: file already exists, mkdir 'temp' at Object.mkdirSync (node:fs:1349:3) at Object.<anonymous> (/home/ayo/dev/demo/main.js:3:4) at Module._compile (node:internal/modules/cjs/loader:1099:14) at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10) at Module.load (node:internal/modules/cjs/loader:975:32) at Function.Module._load (node:internal/modules/cjs/loader:822:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12) at node:internal/main/run_main_module:17:47 { errno: -17, syscall: 'mkdir', code: 'EEXIST', path: 'temp' }
برای error handling این قضیه باید قبل از ساخت دایرکتوری وجود اون رو چک کنیم. میتونیم از ()fs.existsSync استفاده کنیم و طبق اون ادامه بدیم:
if (!fs.existsSync('temp')) { fs.mkdirSync('temp', (err) => { if (err) throw err; }); }
و در آخر:
و همونطور که قبلا هم گفتم پیش بینی و برنامه ریزی برای خطاهای احتمالی و error handling مناسب بشدت مهمه. امیدوارم با توضیحاتی که دادم تونسته باشم تا حدی دید مناسبی از این قضیه رو براتون فراهم کنم تا موقع نوشتن برنامهها به خطاهای احتمالی و error handling(مدیریت خطا) اونها فکر کنید. لطفا نظراتتون برام کامنت کنید تا بتونم موضوعات مورد علاقه شما رو بفهمم و سعی کنم بهتر بنویسم.