در این درس میخوانید
وقتی برنامه نتیجهٔ اشتباه میدهد، اولین واکنش نباید تغییر تصادفی چند خط باشد. یک روش تکرارپذیر لازم داریم: مشکل را بازتولید کنیم، ورودی و انتظار را بنویسیم، نقطهٔ اختلاف را پیدا کنیم و پس از اصلاح دوباره همان حالت را اجرا کنیم. درس سیزدهم روی همین فرایند تمرکز دارد. مثالهای محاسباتی در Console یا فایل مستقل اجرا میشوند؛ بخش breakpoint را در ابزار توسعهٔ مرورگر تمرین میکنیم.
سه دستهٔ خطا را از هم جدا کنید
- Syntax error: کد از نظر قواعد زبان قابل تجزیه نیست؛ مانند پرانتز بستهنشده.
- Runtime error: برنامه شروع شده اما هنگام اجرا مشکلی رخ میدهد؛ مانند خواندن ویژگی null.
- Logic error: برنامه اجرا میشود، ولی نتیجه با قاعدهٔ مورد انتظار سازگار نیست؛ مانند اتصال رشته بهجای جمع عددی.
خطای نحویِ خود فایل معمولاً پیش از اجرای همان فایل شناسایی میشود؛ گذاشتن try در همان کدِ تجزیهناپذیر آن را نجات نمیدهد. خطای منطقی نیز الزاماً exception تولید نمیکند. پیام Console، شمارهٔ خط و Stack Trace سرنخاند؛ آنها را از اولین خطای مرتبط بخوانید، چون خطای بعدی ممکن است فقط پیامد خطای اول باشد. برای گزارش باگ، نسخهٔ محیط و کوچکترین ورودیِ بازتولیدکننده را ثبت کنید.
try/catch برای خطای قابل انتظار
const raw = "{ invalid json }";
try {
const data = JSON.parse(raw);
console.log(data);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.log("متن JSON معتبر نیست");
console.debug(message);
}JSON.parse ممکن است برای متن نامعتبر خطا بدهد؛ این یک مرز منطقی برای try/catch است. پیام کاربر باید قابل فهم باشد و جزئیات تشخیصی فقط در محل مناسب ثبت شوند. دادهٔ شخصی، رمز و توکن را وارد log نکنید. catch خالی باعث میشود برنامه ظاهراً ادامه دهد، اما علت خرابی گم شود. اگر نتیجه ضروری است، مقدار پیشفرض جعلی نسازید؛ خطا را با قرارداد روشن برگردانید یا به لایهٔ مسئول بسپارید.
خطا را با پیام معنادار بسازید
برای ورودی نامعتبر میتوانید Error یا زیرنوع مناسب آن را پرتاب کنید. پیام باید به علت اشاره کند و نام تابع نیز روشن باشد. throw کردن رشته بهجای شیء Error، اطلاعات تشخیصی استاندارد را ضعیف میکند. در مثال زیر، تابع فقط متنِ غیرخالیِ قابل تبدیل به عدد متناهی را میپذیرد. این قرارداد دربارهٔ قیمت غیرمنفی است؛ اگر واحد پول عدد صحیح میخواهد، صحیحبودن را نیز جدا بررسی کنید.
function parsePrice(raw) {
if (typeof raw !== "string" || raw.trim() === "") {
throw new TypeError("Price must be non-empty text");
}
const value = Number(raw);
if (!Number.isFinite(value) || value < 0) {
throw new RangeError("Price must be a finite non-negative number");
}
return value;
}
try {
console.log(parsePrice("25000")); // 25000
parsePrice("abc");
} catch (error) {
console.log(error instanceof Error ? error.name : "Unknown error");
// RangeError
}یک باگ منطقی را عمداً بازتولید کنید
function totalWithShipping(price, shipping) {
return price + shipping;
}
console.log(totalWithShipping("25000", 5000)); // "250005000"انتظار ما عدد 30000 است، اما ورودی price متن است. در DevTools بخش Sources، فایل را باز کنید و روی خط return یک breakpoint بگذارید. نمونه را دوباره اجرا کنید. در حالت توقف، نوع و مقدار price و shipping را ببینید، سپس با Step Over مسیر را ادامه دهید. برای این باگ، مسئله در مرز تبدیل داده است؛ افزودن یک try/catch نتیجه را درست نمیکند، چون اتصال رشته از نظر زبان مجاز است.
- ورودی دقیق و نتیجهٔ مورد انتظار را یادداشت کنید.
- روی خط پیش از نتیجهٔ اشتباه توقف بگذارید.
- مقدار و typeof متغیرها را در همان لحظه بررسی کنید.
- پس از اصلاح، همان ورودی و چند حالت مرزی را دوباره اجرا کنید.
- breakpoint و log موقتِ غیرضروری را پیش از انتشار جمع کنید.
اصلاح در مرز مناسب
اگر قرارداد totalWithShipping دریافت عدد است، تبدیل متن را پیش از فراخوانی و با اعتبارسنجی انجام دهید. اگر قرار است خود تابع متن بپذیرد، نام و قرارداد آن را متناسب تغییر دهید. از تبدیل خام Number روی رشتهٔ خالی بدون بررسی پرهیز کنید. همچنین stack trace محل بروز مشکل را نشان میدهد، اما علت ممکن است چند مرحله قبل، در ورود داده، ایجاد شده باشد.
function checkedTotal(price, shipping) {
if (!Number.isFinite(price) || !Number.isFinite(shipping)) {
throw new TypeError("Numeric amounts required");
}
return price + shipping;
}
console.log(checkedTotal(Number("25000"), 5000)); // 30000تمرین و پاسخ پیشنهادی
تابعی بنویسید که یک مقدار را بهعنوان تعداد درس بگیرد و فقط عدد صحیحِ صفر یا بیشتر را بپذیرد. آن را با 0، 3، -1، 2.5 و رشتهٔ «3» آزمایش کنید. این بار قرارداد، پذیرش خود عدد است و رشته باید رد شود. پاسخ زیر مقدار معتبر را برمیگرداند و برای بقیه خطا میسازد؛ پیام رابط را لایهٔ فراخواننده تعیین میکند.
function requireLessonCount(value) {
if (!Number.isInteger(value) || value < 0) {
throw new RangeError("Lesson count must be a non-negative integer");
}
return value;
}
console.log(requireLessonCount(0)); // 0
console.log(requireLessonCount(3)); // 3
// بقیهٔ ورودیهای تمرین را جداگانه داخل try/catch بررسی کنید.مرز خطای ناهمگام
try/catch فقط با قراردادن ظاهری یک عملیات ناهمگام داخل try، همهٔ خطاهای آینده را نمیگیرد. برای Promise باید زنجیرهٔ catch داشته باشید یا نتیجه را با await درون try منتظر بمانید. این تفاوت را در دو درس بعد با ترتیب اجرا و نمونههای کامل تمرین میکنیم. finally برای پاکسازی وضعیت مفید است، اما return از داخل finally میتواند نتیجه یا خطای قبلی را بپوشاند؛ چنین رفتاری را بیدلیل وارد کد نکنید.
منابع انگلیسی
- MDN — Control flow and error handling
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Control_flow_and_error_handling
- Chrome DevTools — Debug JavaScript
- https://developer.chrome.com/docs/devtools/javascript
- MDN — Error
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
سناریوی قیمت و تمرین خطایابی برای این مجموعه طراحی شدهاند. در ادامه، Promise و Event Loop را یاد میگیریم تا ترتیب کارهای ناهمگام روشن شود.
پیشنیاز و محیط اجرای مثالها
درس 13 از ۲۰؛ پیشنیاز: درسهای قبلی مجموعه. کدهای مستقل این درس در Console تازهٔ مرورگر یا فایل JavaScript اجرا میشوند؛ نمونهها را جداگانه آزمایش کنید.
پیشرفت روی همین مرورگر ذخیره میشود.