۸. خطا بخشی از قرارداد است؛ Error اختصاصی و نتیجهٔ صریح
کاربر فایل پشتیبان خراب انتخاب میکند. اگر برنامه فقط پیام «مشکلی پیش آمد» بدهد، نمیداند فایل اشتباه است یا ذخیرهسازی مرورگر مسدود شده. اگر هم در catch آرایهٔ خالی برگردانیم و آن را ذخیره کنیم، ممکن است دادهٔ درست قبلی از بین برود. در سطح متوسط، خطا را بخشی از جریان برنامه طراحی میکنیم و موفقیت ساختگی نمیسازیم.
در این درس دو سبک را میبینید: پرتاب خطای مشخص برای شکست عملیات و برگرداندن نتیجهٔ صریح برای بررسیهای قابل انتظار. هیچکدام برای همهٔ مسئلهها برتر نیست. مهم این است که فراخواننده بداند نتیجه چه شکلی دارد و در هر حالت چه کاری باید انجام دهد. مثالها مستقلاند و در Node.js ۲۴ یا مرورگر جدید اجرا میشوند.
خطای دامنه با شناسهٔ قابل بررسی
class ValidationError extends Error {
constructor(code, message, options) {
super(message, options);
this.name = 'ValidationError';
this.code = code;
}
}
function requireTitle(raw) {
if (typeof raw !== 'string' || raw.trim().length < 2) {
throw new ValidationError('TITLE_SHORT', 'عنوان دستکم دو نویسه لازم دارد.');
}
if (raw.trim().length > 100) {
throw new ValidationError('TITLE_LONG', 'عنوان بیش از حد بلند است.');
}
return raw.trim();
}
try {
requireTitle(' ');
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.code); // TITLE_SHORT
console.log(error.message); // عنوان دستکم دو نویسه لازم دارد.
} else {
throw error;
}
}شناسهٔ code برای تصمیم برنامه و پیام برای توضیح به کاربر است. مقایسهٔ متن پیام، برنامه را به ویرایش نگارشی یا ترجمه وابسته میکند. در این مثال، instanceof در همان محیط اجرا استفاده میشود. وقتی خطا از شبکه به شکل JSON میآید، دیگر نباید فرض کنید نمونهٔ همین کلاس است؛ پاسخ شبکه به قرارداد و اعتبارسنجی خودش نیاز دارد.
دلیل بازپرتاب خطای ناشناخته این است که همهٔ خطاها مشکل ورودی نیستند. خطای تایپی برنامهنویس نباید به پیام «عنوان کوتاه است» تبدیل شود. در لایهٔ رابط میتوان پیام عمومی مناسبی نشان داد، ولی برای عیبیابی باید علت محفوظ بماند. جزئیات داخلی و پشتهٔ خطا را بهصورت عمومی در صفحهٔ کاربر منتشر نکنید.
نگهداری علت اصلی هنگام افزودن زمینه
function decodeBackup(text) {
let payload;
try {
payload = JSON.parse(text);
} catch (cause) {
throw new Error('فایل، JSON معتبر نیست.', { cause });
}
if (!payload || payload.version !== 1 || !Array.isArray(payload.items)) {
throw new Error('ساختار یا نسخهٔ پشتیبان پشتیبانی نمیشود.');
}
return payload;
}
try {
decodeBackup('{broken');
} catch (error) {
console.log(error.message); // فایل، JSON معتبر نیست.
console.log(error.cause instanceof SyntaxError); // true
}این تابع فقط پوستهٔ پشتیبان را بررسی میکند؛ بررسی هر ردیف، شناسههای تکراری و مجموع مبلغ هنوز لازم است. در پروژهٔ دفتر هزینه، restore بخش بیشتری از این قرارداد را اجرا میکند. اگر مثال کوتاه آموزشی را جای آن بگذارید، اعتبارسنجی ضعیفتر میشود. هر نمونه را با محدودهٔ مسئولیت اعلامشدهاش بخوانید.
پشتیبان نامعتبر نباید وضعیت فعال را جایگزین کند. یک ترتیب امنتر برای این تمرین چنین است: خواندن متن، تجزیه، اعتبارسنجی کامل در یک متغیر موقت، نمایش نتیجهٔ مورد انتظار و سپس جایگزینی. در صورت شکست هر مرحله، دادهٔ فعلی باید باقی بماند. طراحی بازیابی در درس ذخیرهسازی با جزئیات بیشتری دنبال میشود.
نتیجهٔ صریح برای خطای قابل انتظار فرم
function checkTicketTitle(raw) {
if (typeof raw !== 'string') {
return { ok: false, field: 'title', message: 'عنوان را وارد کنید.' };
}
const value = raw.trim();
if (value.length < 2 || value.length > 100) {
return { ok: false, field: 'title', message: 'عنوان باید ۲ تا ۱۰۰ نویسه باشد.' };
}
return { ok: true, value };
}
const result = checkTicketTitle(' خطای نصب ');
if (result.ok) console.log(result.value); // خطای نصب
else console.log(result.message);فراخواننده پیش از خواندن value، علامت ok را بررسی میکند. مزیت این قرارداد در فرم این است که ورودی نامعتبر یک حالت عادی قابل نمایش است. عیب احتمالی، فراموشکردن بررسی نتیجه است؛ بنابراین در مرور کد و آزمونها باید آن را پیگیری کنید. ترکیب بیقاعدهٔ null، متن خطا، عدد و شیء موفق در خروجی یک تابع، فهم آن را دشوار میکند.
پرتاب خطا و نتیجهٔ صریح را در یک عملیات بدون مستندکردن مخلوط نکنید. مثلاً میتوان قرارداد را چنین تعیین کرد: خطای اعتبارسنجی به شکل نتیجه، شکست زیرساخت با Error. این انتخاب زمانی مفید است که همهٔ فراخوانندهها همان انتظار را داشته باشند و مسیر ناشناخته نیز مدیریت شود.
در رابط، زمان نمایش پیام هم بخشی از طراحی است. اگر کاربر هنوز در حال تایپ عنوان است، نمایش خطای بلند پس از هر نویسه ممکن است مزاحم باشد؛ میتوانید بررسی نهایی را هنگام ارسال و راهنمای کوتاه را کنار فیلد قرار دهید. پس از شکست ارسال، پیشنویس را حفظ کنید. کاربر باید بتواند مشکل را اصلاح کند و دوباره بفرستد، بدون آنکه مجبور به بازنویسی همهٔ اطلاعات شود. این رفتار را همراه مسیر موفق آزمایش کنید.
تمرین، پاسخ و معیار تحویل
برای ورود پشتیبان سه شکست بسازید: JSON خراب، نسخهٔ ناشناخته و ردیف دارای مبلغ نامعتبر. پیامها باید متفاوت و قابل اقدام باشند، ولی در هر سه حالت دادهٔ قبلی باقی بماند. سپس شکست ذخیرهسازی را جدا شبیهسازی کنید؛ فایل درست ممکن است خوانده شود اما ذخیرهٔ پایدار موفق نباشد.
پاسخ راهنما: مرحلهها را به تابعهای کوچک جدا کنید و نتیجهٔ معتبر را فقط پس از پایان همهٔ بررسیها اعمال کنید. برای نگهداری علت از cause و برای تصمیم دامنه از شناسهٔ مشخص استفاده کنید. معیار تحویل، نداشتن موفقیت جعلی، حفظ ورودی کاربر، حفظ دادهٔ قبلی و امکان تلاش دوباره است. توضیح دهید چرا catch { return []; } برای شکست بازیابی انتخاب خطرناکی بود.
منابع رسمی برای مطالعهٔ تکمیلی
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
تألیف و مثالهای اختصاصی: تحریریه درسآوند.