Unhandled Promise Rejection: خطای خاموشی که کاربر می‌بیند و شما نه

Unhandled Promise Rejection: خطای خاموشی که کاربر می‌بیند و شما نه
در این مقاله می‌خوانید
  1. یعنی چه؟
  2. چرا این‌قدر رایج است
  3. ۱. فراموش کردن catch در زنجیره
  4. ۲. تابع async که صدا زده می‌شود ولی منتظرش نمی‌مانند
  5. ۳. خطای داخل then
  6. ۴. فرض اینکه fetch در برابر خطای سرور رد می‌شود
  7. ۵. Promise.all و شکست یکی از اعضا
  8. چطور بگیریمشان
  9. در سطح موضعی
  10. در سطح سراسری
  11. چرا کاربران گزارش نمی‌دهند
  12. چطور موجودی‌های فعلی را پیدا کنیم
  13. در Node.js فرق می‌کند
  14. الگوهایی که مشکل را کم می‌کنند
  15. جمع‌بندی
  16. منابع و مطالعهٔ بیشتر

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

یعنی چه؟

هر Promise دو سرنوشت دارد: یا موفق می‌شود، یا رد می‌شود. وقتی رد می‌شود و هیچ‌کس catchای برایش نگذاشته باشد، جاوااسکریپت رویداد unhandledrejection را منتشر می‌کند.

fetch('/api/user')
  .then(r => r.json())
  .then(u => render(u));
  // اگر شبکه قطع باشد، اینجا هیچ catch ای نیست

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

چرا این‌قدر رایج است

۱. فراموش کردن catch در زنجیره

ساده‌ترین حالت. هر زنجیرهٔ then که به catch ختم نشود، یک بمب ساعتی است.

۲. تابع async که صدا زده می‌شود ولی منتظرش نمی‌مانند

async function saveDraft() { await api.save(draft); }

saveDraft();          // بدون await و بدون catch — رد شدنش بی‌صدا می‌ماند

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

۳. خطای داخل then

fetch('/api/user')
  .then(r => r.json())
  .then(u => document.getElementById('name').textContent = u.name)
  .catch(e => console.log('خطای شبکه'));   // پیام گمراه‌کننده

اگر عنصر name وجود نداشته باشد، یک TypeError رخ می‌دهد که وارد همان catch می‌شود و به‌عنوان «خطای شبکه» گزارش می‌شود. catch انتهای زنجیره همه‌چیز را می‌گیرد، نه فقط خطای شبکه را.

۴. فرض اینکه fetch در برابر خطای سرور رد می‌شود

مهم‌ترین سوءتفاهم دربارهٔ fetch: پاسخ ۵۰۰ یا ۴۰۴ باعث رد شدن Promise نمی‌شود. Promise با موفقیت resolve می‌شود و شما باید خودتان بررسی کنید:

const res = await fetch('/api/user');
if (!res.ok) throw new Error('HTTP ' + res.status);   // بدون این خط، خطا خاموش می‌ماند
const user = await res.json();

بدون آن خط، res.json() روی صفحهٔ خطای HTML اجرا می‌شود و خطای مبهم Unexpected token می‌دهد — که هیچ ربطی به مشکل واقعی ندارد.

۵. Promise.all و شکست یکی از اعضا

Promise.all با اولین شکست رد می‌شود. اگر می‌خواهید بقیه ادامه دهند، Promise.allSettled ابزار درست است.

چطور بگیریمشان

در سطح موضعی

try {
  const res = await fetch('/api/user');
  if (!res.ok) throw new Error('HTTP ' + res.status);
  render(await res.json());
} catch (e) {
  showError('بارگذاری اطلاعات ممکن نشد');   // به کاربر بگویید چه شد
  report(e);                                 // و خودتان هم بدانید
}

نکتهٔ کلیدی: هر دو کار را بکنید. فقط ثبت کردن یعنی کاربر همچنان اسپینر می‌بیند؛ فقط پیام دادن یعنی شما هرگز نمی‌فهمید چند نفر به این خورده‌اند.

در سطح سراسری

تور ایمنی برای هر چیزی که از دستتان در رفته:

window.addEventListener('unhandledrejection', function (e) {
  report({ type: 'unhandled_rejection', reason: String(e.reason), stack: e.reason?.stack });
});

و این نکته را حتماً بدانید: این رویداد جدا از window.onerror است. اگر فقط onerror را گوش می‌کنید، هیچ‌کدام از این خطاها را نمی‌بینید. در کد امروزی که بیشتر منطقش ناهم‌زمان است، این یعنی بخش بزرگی از خطاهایتان نامرئی می‌مانند.

چرا کاربران گزارش نمی‌دهند

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

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

چطور موجودی‌های فعلی را پیدا کنیم

احتمالاً همین حالا چند مورد از این‌ها در کدتان هست. سه راه برای پیدا کردنشان:

۱. در کنسول مرورگر بگردید. این خطاها در کنسول با متن Uncaught (in promise) ظاهر می‌شوند. یک بار مسیرهای اصلی سایت را با کنسول باز طی کنید — معمولاً چند مورد پیدا می‌شود.

۲. دنبال الگوهای مشکوک بگردید. در کد جستجو کنید:

.then(          # هر زنجیره‌ای که به catch ختم نشود
async function  # و هر تابعی که بدون await صدا زده شود

هر fetch که بعدش res.ok بررسی نشده، مورد بعدی است.

۳. شنوندهٔ سراسری را اضافه کنید و یک هفته صبر کنید. مؤثرترین راه، چون کاربران واقعی مسیرهایی می‌روند که شما نمی‌روید.

در Node.js فرق می‌کند

اگر کد سمت سرور هم دارید، بدانید که رفتار متفاوت است. در نسخه‌های جدید Node، یک Promise رد شده و مدیریت‌نشده باعث می‌شود کل پراسس متوقف شود — نه اینکه بی‌صدا رد شود. معادل مرورگری آن این است:

process.on('unhandledRejection', (reason) => {
  logger.error({ reason }, 'unhandled rejection');
  // در محیط production معمولاً بهتر است بعد از ثبت، تمیز خارج شوید
});

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

الگوهایی که مشکل را کم می‌کنند

  • هر تابع async که در پس‌زمینه صدا زده می‌شود، catch داشته باشد. اگر await نمی‌کنید، دست‌کم .catch(report) بگذارید.
  • res.ok را همیشه بررسی کنید. یا یک تابع کمکی بنویسید که این کار را خودکار کند و همه‌جا از آن استفاده کنید.
  • حالت خطا را در رابط کاربری طراحی کنید. هر جایی که اسپینر دارید، باید حالت «نشد» هم داشته باشد — با امکان تلاش دوباره.
  • تایم‌اوت بگذارید. درخواستی که هرگز جواب نمی‌دهد بدتر از درخواستی است که شکست می‌خورد؛ دست‌کم با تایم‌اوت تبدیل به خطای قابل مدیریت می‌شود.
  • در catch پیام عمومی ننویسید. «خطای شبکه» وقتی مشکل چیز دیگری است، ساعت‌ها وقت دیباگ می‌گیرد.

جمع‌بندی

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

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

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

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

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

شروع رایگان