راهنمای جامع خطاهای جاوااسکریپت: از شناسایی تا رفع

راهنمای جامع خطاهای جاوااسکریپت: از شناسایی تا رفع
در این مقاله می‌خوانید
  1. خطای جاوااسکریپت دقیقاً چیست؟
  2. تفاوت خطا با استثنا
  3. چرا بیشتر خطاها را هرگز نمی‌بینید
  4. هفت خانوادهٔ اصلی خطا
  5. ۱. TypeError
  6. ۲. ReferenceError
  7. ۳. SyntaxError
  8. ۴. RangeError
  9. ۵. خطاهای شبکه
  10. ۶. Promise Rejection مدیریت‌نشده
  11. ۷. خطاهای خاموش
  12. چطور یک خطا را بخوانیم
  13. از محیط توسعه تا production
  14. چک‌لیست عملی
  15. سه الگوی مدیریت خطا و جای درست هرکدام
  16. لایهٔ اول: try/catch موضعی
  17. لایهٔ دوم: مرز خطا در سطح کامپوننت
  18. لایهٔ سوم: تور ایمنی سراسری
  19. کدام خطاها را اول حل کنیم؟
  20. خطاهایی که مال شما نیستند
  21. قدم بعدی
  22. منابع و مطالعهٔ بیشتر

یک واقعیت ناخوشایند دربارهٔ خطاهای جاوااسکریپت وجود دارد: برخلاف خطاهای سمت سرور، این خطاها در لاگ شما ثبت نمی‌شوند. آن‌ها در مرورگر کاربر اتفاق می‌افتند، روی دستگاهی که هرگز نمی‌بینید، و در بیشتر موارد کاربر فقط شانه بالا می‌اندازد و صفحه را می‌بندد. سایت از دید شما «بالا» است و مانیتورینگ سرور هم سبز است — ولی دکمهٔ پرداخت برای ۱۲ درصد کاربران کار نمی‌کند.

این راهنما نقشهٔ کامل موضوع است: خطای جاوااسکریپت چیست، چند خانوادهٔ اصلی دارد، چطور آن را بخوانید، و چطور از حالت «نمی‌دانم چه خبر است» به حالت «دقیقاً می‌دانم کدام کاربر، در کدام صفحه، به چه خطایی خورد» برسید.

خطای جاوااسکریپت دقیقاً چیست؟

وقتی مرورگر در حال اجرای کد شماست و به دستوری می‌رسد که نمی‌تواند اجرا کند، اجرای آن بلوک کد را متوقف می‌کند و یک شیء خطا (Error object) می‌سازد. اگر کسی آن خطا را نگیرد، به بالاترین سطح می‌رسد و در کنسول ثبت می‌شود.

نکتهٔ مهم اینجاست: توقف اجرا فقط شامل همان زنجیرهٔ فراخوانی می‌شود، نه کل صفحه. اگر خطا داخل هندلر کلیک یک دکمه رخ دهد، بقیهٔ صفحه سالم می‌ماند و فقط آن دکمه بی‌اثر می‌شود. کاربر صفحه‌ای می‌بیند که ظاهراً درست است ولی به کلیک او واکنش نشان نمی‌دهد. این بدترین حالت ممکن است، چون نه کاربر می‌فهمد چه شد و نه شما.

تفاوت خطا با استثنا

در جاوااسکریپت این دو واژه معمولاً به‌جای هم به کار می‌روند، ولی تفکیکشان مفید است: Error شیئی است که ساخته می‌شود و اطلاعات مشکل را حمل می‌کند؛ Exception اتفاقی است که موقع پرتاب شدن آن شیء و بالا رفتنش در پشتهٔ فراخوانی می‌افتد. شما شیء Error می‌سازید، ولی exception را مدیریت می‌کنید.

چرا بیشتر خطاها را هرگز نمی‌بینید

سه دلیل ساختاری باعث می‌شود خطاهای فرانت‌اند از دید تیم پنهان بمانند:

  • محیط اجرا مال شما نیست. کد روی مرورگر کاربر اجرا می‌شود، با نسخه‌ای از کروم که شما تست نکرده‌اید، با افزونه‌هایی که DOM را دستکاری می‌کنند، روی شبکه‌ای که وسط درخواست قطع می‌شود.
  • کاربران گزارش نمی‌دهند. تجربه نشان می‌دهد اکثریت قاطع کاربرانی که به مشکل می‌خورند، هیچ‌وقت آن را گزارش نمی‌کنند؛ فقط می‌روند. کسی که گزارش می‌دهد استثناست، نه قاعده.
  • تست‌ها این حالت‌ها را پوشش نمی‌دهند. تست‌های شما با دادهٔ تمیز و شبکهٔ سالم اجرا می‌شوند. باگ‌های واقعی معمولاً از دادهٔ غیرمنتظره یا شرایط شبکهٔ بد می‌آیند.

نتیجه این می‌شود که فاصله‌ای بین «سایت کار می‌کند» و «سایت برای کاربران کار می‌کند» شکل می‌گیرد، و تنها راه دیدن این فاصله، ثبت خودکار خطاها از سمت مرورگر است.

هفت خانوادهٔ اصلی خطا

۱. TypeError

رایج‌ترین خطای جاوااسکریپت. یعنی روی مقداری عملیاتی انجام داده‌اید که آن مقدار پشتیبانی نمی‌کند — مثلاً خواندن یک ویژگی از undefined یا صدا زدن چیزی که تابع نیست.

const user = data.user;      // data.user is undefined
console.log(user.name);      // TypeError

تقریباً همیشه ریشه‌اش این است که فرض کرده‌اید داده‌ای وجود دارد که در آن لحظه هنوز نرسیده یا اصلاً نیامده است.

۲. ReferenceError

به متغیری اشاره کرده‌اید که اصلاً تعریف نشده. معمولاً غلط املایی، فراموش کردن import، یا استفاده از متغیری پیش از تعریفش در بلوک let/const.

۳. SyntaxError

کد از نظر دستوری معتبر نیست. این خانواده معمولاً موقع توسعه گرفته می‌شود، ولی یک حالت خطرناک دارد: وقتی سرور به‌جای JSON یک صفحهٔ HTML خطا برمی‌گرداند و JSON.parse روی آن اجرا می‌شود.

۴. RangeError

مقدار در محدودهٔ مجاز نیست. معروف‌ترین نمونه‌اش Maximum call stack size exceeded است که یعنی بازگشت بی‌نهایت دارید.

۵. خطاهای شبکه

درخواست fetch یا XHR که شکست می‌خورد یا وضعیت ۴xx و ۵xx برمی‌گرداند. نکتهٔ مهم: fetch در برابر پاسخ ۵۰۰ خطا پرتاب نمی‌کند — قول (Promise) با موفقیت resolve می‌شود و شما باید خودتان response.ok را بررسی کنید. این یکی از پرتکرارترین منابع باگ خاموش است.

۶. Promise Rejection مدیریت‌نشده

یک قول رد می‌شود و هیچ catchای برایش وجود ندارد. این خطا اجرای بقیهٔ صفحه را متوقف نمی‌کند و در بسیاری از تنظیمات حتی به window.onerror هم نمی‌رسد؛ رویداد جداگانه‌ای به نام unhandledrejection دارد.

۷. خطاهای خاموش

خطرناک‌ترین خانواده، چون اصلاً خطایی پرتاب نمی‌شود. یک catch خالی که مشکل را می‌بلعد، یا منطقی که به‌جای خطا مقدار اشتباه برمی‌گرداند. از دید سیستم همه‌چیز عادی است؛ فقط عدد نمایش‌داده‌شده به کاربر غلط است.

چطور یک خطا را بخوانیم

یک خطای جاوااسکریپت سه بخش اطلاعاتی دارد و بیشتر توسعه‌دهنده‌ها فقط بخش اول را می‌خوانند:

  • نوع و پیام — چه اتفاقی افتاد.
  • Stack trace — از کجا آمد. این را از بالا به پایین بخوانید: خط اول جایی است که خطا رخ داد، خطوط بعدی مسیری است که به آنجا رسید. اولین خطی که به کد خودتان اشاره می‌کند (نه به کتابخانه‌ها) معمولاً همان جایی است که باید نگاه کنید.
  • زمینه — کاربر چه کرد که به اینجا رسید. این بخش در پیام خطا نیست و باید جداگانه ثبت شود؛ به همین دلیل ابزارهای رصد خطا مفهومی به نام breadcrumb دارند که مسیر کلیک‌ها و درخواست‌های قبل از خطا را نگه می‌دارد.

بدون بخش سوم، بسیاری از خطاها غیرقابل بازتولید می‌مانند. می‌دانید چه خطایی رخ داده ولی نمی‌دانید چطور دوباره ایجادش کنید.

از محیط توسعه تا production

در محیط توسعه، کنسول باز است و خطا را بلافاصله می‌بینید. در production سه چیز عوض می‌شود:

کد فشرده شده است. stack trace به a.js:1:24817 اشاره می‌کند که هیچ معنایی ندارد. راه‌حل، تولید و بارگذاری Source Map است تا خطا به فایل و خط اصلی نگاشت شود.

کنسولی وجود ندارد که کسی ببیندش. خطا در مرورگر کاربر ثبت می‌شود و همان‌جا می‌ماند. باید مکانیزمی داشته باشید که آن را به سرور شما بفرستد.

حجم زیاد است. یک خطا در صفحهٔ پربازدید می‌تواند روزی هزاران بار تکرار شود. بدون گروه‌بندی هوشمند (fingerprint)، با سیلی از رخدادهای تکراری روبه‌رو می‌شوید که سیگنال واقعی را دفن می‌کند.

چک‌لیست عملی

اگر می‌خواهید از امروز وضعیت خطاهای سایتتان را بهتر کنید، به این ترتیب پیش بروید:

  • یک تور ایمنی سراسری بگذارید. window.onerror و رویداد unhandledrejection را گوش کنید تا هیچ خطایی بی‌صدا رد نشود.
  • پاسخ‌های شبکه را واقعاً بررسی کنید. بعد از هر fetch، مقدار response.ok را چک کنید. نبود این بررسی، منبع بخش بزرگی از باگ‌های خاموش است.
  • catch خالی ننویسید. اگر خطایی را می‌گیرید و کاری با آن ندارید، دست‌کم ثبتش کنید.
  • Source Map تولید کنید. بدون آن، خطای production عملاً بی‌فایده است.
  • زمینه را ثبت کنید. بدانید کاربر پیش از خطا در چه صفحه‌ای بود و چه کلیکی کرد.
  • از کاربر بپرسید. بخشی از مشکلات اصلاً خطای فنی نیستند — دکمه‌ای که پیدا نمی‌شود یا متنی که گیج‌کننده است. برای این‌ها به مسیر مستقیم دریافت بازخورد نیاز دارید.

سه الگوی مدیریت خطا و جای درست هرکدام

مدیریت خطا در جاوااسکریپت سه لایه دارد و اشتباه رایج این است که فقط یکی را به کار می‌برند.

لایهٔ اول: try/catch موضعی

برای جایی که می‌دانید چه چیزی ممکن است خراب شود و می‌دانید در آن صورت چه کار کنید. مثلاً خواندن از localStorage که در حالت مرور خصوصی استثنا پرتاب می‌کند:

function readDraft() {
  try {
    return localStorage.getItem('draft');
  } catch (e) {
    return null;   // حالت خصوصی مرورگر؛ پیش‌نویس نداریم و مشکلی نیست
  }
}

این کاربرد درست است، چون بازگشت به حالت امن مشخصی دارید. اما اگر catch شما خالی است یا فقط یک console.log دارد، در واقع دارید خطا را پنهان می‌کنید نه مدیریت. قاعدهٔ ساده: اگر نمی‌دانید در catch چه بنویسید، احتمالاً آنجا نباید catch بگذارید.

لایهٔ دوم: مرز خطا در سطح کامپوننت

در اپلیکیشن‌های تک‌صفحه‌ای، یک خطا در گوشه‌ای از رابط کاربری می‌تواند کل درخت را از کار بیندازد و صفحهٔ سفید بدهد. فریم‌ورک‌ها برای همین مفهوم مرز خطا را دارند: بخشی از رابط کاربری را در کپسولی قرار می‌دهید تا اگر شکست، فقط همان بخش جای خود را به یک پیام جایگزین بدهد و بقیهٔ صفحه سالم بماند.

لایهٔ سوم: تور ایمنی سراسری

هر چقدر هم دقیق باشید، خطاهایی از دو لایهٔ قبل رد می‌شوند. لایهٔ آخر یک شنوندهٔ سراسری است که هر چیزی را که به بالاترین سطح رسیده می‌گیرد و ثبت می‌کند:

window.addEventListener('error', function (e) {
  report({ message: e.message, stack: e.error && e.error.stack, url: e.filename });
});

window.addEventListener('unhandledrejection', function (e) {
  report({ message: 'Unhandled rejection', reason: String(e.reason) });
});

توجه کنید که این دو رویداد جدا هستند. اگر فقط اولی را گوش کنید، تمام promiseهای رد شده را از دست می‌دهید — و در کدهای مدرن که همه‌چیز ناهم‌زمان است، این معمولاً بخش بزرگ‌تری از خطاهاست.

کدام خطاها را اول حل کنیم؟

وقتی برای اولین بار سیستم ثبت خطا راه می‌اندازید، معمولاً با فهرستی روبه‌رو می‌شوید که دلسردکننده است. مرتب‌سازی بر اساس تعداد رخداد، اشتباه رایجی است: یک خطا در حلقه‌ای که هر ثانیه اجرا می‌شود، بالای فهرست می‌نشیند بدون آنکه واقعاً مهم باشد.

معیارهای بهتر، به ترتیب اهمیت:

  • تعداد کاربران متأثر، نه تعداد رخداد. خطایی که ۵۰ کاربر مختلف دیده‌اند، از خطایی که ۵۰۰ بار برای یک نفر تکرار شده مهم‌تر است.
  • نزدیکی به مسیر درآمد. خطا در صفحهٔ پرداخت با خطا در صفحهٔ «دربارهٔ ما» قابل مقایسه نیست، حتی اگر دومی صد برابر بیشتر رخ دهد.
  • تازگی. خطایی که از دیروز شروع شده، احتمالاً نتیجهٔ انتشار اخیر شماست و رفعش ساده‌تر است، چون هنوز یادتان هست چه تغییری دادید.
  • بازگشت پس از رفع. باگی که حل کرده بودید و برگشته، معمولاً یعنی رفع قبلی علامت را پوشانده بود نه ریشه را.

و یک توصیهٔ عملی: در هفتهٔ اول چیزی را حل نکنید. فقط تماشا کنید. بسیاری از خطاهایی که در روز اول وحشتناک به نظر می‌رسند، خطای افزونه‌های مرورگر یا ربات‌هایند و اصلاً به کد شما ربطی ندارند.

خطاهایی که مال شما نیستند

بخش قابل توجهی از خطاهایی که در مرورگر کاربران ثبت می‌شود، از کد شما نمی‌آید:

  • افزونه‌های مرورگر که DOM صفحه را دستکاری می‌کنند و با کد شما تداخل پیدا می‌کنند.
  • اسکریپت‌های شخص ثالث مثل ابزارهای آماری یا چت آنلاین.
  • خطای مبهم Script error. که وقتی رخ می‌دهد که اسکریپتی از دامنهٔ دیگری خطا داده و مرورگر به‌دلایل امنیتی جزئیاتش را پنهان می‌کند. رفعش این است که روی تگ اسکریپت ویژگی crossorigin بگذارید و سرور مربوطه هدر CORS مناسب بدهد.
  • قطع شبکه وسط بارگذاری، که در شبکه‌های موبایل بسیار رایج است.

این‌ها را فیلتر کنید. اگر نکنید، ظرف چند هفته تیم شما اعلان‌ها را نادیده می‌گیرد و آن‌وقت داشتن یا نداشتن سیستم رصد فرقی نمی‌کند.

قدم بعدی

خطاهای جاوااسکریپت مشکل عجیبی نیستند؛ مشکل این است که دیده نمی‌شوند. تیمی که خطاهایش را می‌بیند، معمولاً ظرف چند روز اکثرشان را رفع می‌کند. تیمی که نمی‌بیند، ماه‌ها با آن‌ها زندگی می‌کند بدون اینکه بداند.

در مقالات این دسته، هر کدام از خطاهای رایجی که بالا معرفی شد را جداگانه و با نمونهٔ کد بررسی می‌کنیم. اگر همین حالا خطای مشخصی در کنسول دارید، احتمالاً پاسخش در همین فهرست هست. تا اینجا این‌ها منتشر شده‌اند: Cannot read properties of undefined، خطای CORS، Unhandled Promise Rejection، Source Map و خوانا کردن خطای production و تور ایمنی سراسری با window.onerror.

منابع و مطالعهٔ بیشتر

باگ‌ماگ را رایگان امتحان کنید

خطاهای سایتتان را خودکار ثبت کنید و بگذارید کاربران با یک کلیک باگ گزارش دهند.

شروع رایگان