مدیریت خطا در کد ناهمزمان و رمزگشایی Stack Trace

در این درس، دو موضوع تکمیلی و کاربردی بررسی می‌شود: خطاهایی که به‌طور خاص در دل عملیات ناهمزمان رخ می‌دهند و ممکن است به‌طور خاموش نادیده گرفته شوند، و نحوه‌ی خواندن دقیق‌تر stack trace برای ردیابی منشأ واقعی یک خطا. این دو مبحث، مکمل مستقیم آنچه درباره‌ی throw/try/catch و خطاهای سفارشی در این فصل آموختید هستند.

خطر Promiseهای بدون catch

اگر یک Promise reject شود و هیچ catch یا try/catch ای برای مدیریت آن نوشته نشده باشد، خطا به‌طور خاموش گم نمی‌شود؛ محیط اجرا (مرورگر یا Node.js) یک هشدار Unhandled Promise Rejection در کنسول ثبت می‌کند:

async function loadData() {
  const response = await fetch("/api/nonexistent-endpoint");
  return response.json();
}

loadData(); // بدون await یا catch در بیرون

// در کنسول مرورگر:
// Uncaught (in promise) TypeError: Failed to fetch

این نوع خطا به‌ویژه خطرناک است چون برنامه معمولاً بدون توقف کامل به کار خود ادامه می‌دهد، در حالی که بخشی از منطق برنامه (مثل به‌روزرسانی رابط کاربری با داده‌ی دریافتی) هرگز اجرا نشده است.

شنیدن سراسری Unhandled Rejection

برای مانیتورینگ و لاگ‌گیری متمرکز، مرورگر رویدادی به نام unhandledrejection روی شیء window ارائه می‌دهد که امکان دریافت این نوع خطاها را در سطح کل برنامه فراهم می‌کند:

مدیریت سراسری Promise رهاشده
خروجی کنسول اینجا نمایش داده می‌شود...

این تکنیک معمولاً برای ارسال خطاهای مدیریت‌نشده به یک سرویس مانیتورینگ خطا (مثل Sentry) در پروژه‌های واقعی استفاده می‌شود، نه به‌عنوان جایگزینی برای نوشتن try/catch مناسب در همان تابع.

مشکل Stack Trace ناقص در کد ناهمزمان

یکی از چالش‌های واقعی دیباگ کردن کد ناهمزمان این است که stack trace یک خطا، گاهی مسیر کامل فراخوانی توابع را نشان نمی‌دهد؛ چون بخشی از این مسیر در Microtask Queue یا Web APIs طی شده و ارتباط مستقیم آن با Call Stack اصلی گاهی از دست می‌رود. خوشبختانه، موتورهای امروزی جاوااسکریپت (مثل V8 در کروم) با پشتیبانی از async stack trace، این مسیر را تا حد زیادی برای توابع async/await حفظ می‌کنند:

async function fetchUser(id) {
  const response = await fetch(`/api/user/${id}`);
  if (!response.ok) {
    throw new Error("کاربر یافت نشد");
  }
  return response.json();
}

async function showUser(id) {
  const user = await fetchUser(id);
  console.log(user);
}

showUser(999).catch((error) => {
  console.log(error.stack);
  // مسیر معمولاً شامل هر دو تابع fetchUser و showUser است
});

افزودن زمینه‌ی بیشتر به خطا هنگام انتقال بین لایه‌ها

گاهی برای درک بهتر منشأ یک خطا، مفید است هنگام گرفتن و دوباره throw کردن آن در یک لایه‌ی بالاتر، اطلاعات زمینه‌ای (context) بیشتری به آن اضافه شود، بدون آنکه پیام یا stack اصلی خطا از بین برود:

async function loadUserDashboard(userId) {
  try {
    return await fetchUser(userId);
  } catch (error) {
    throw new Error(`بارگذاری داشبورد کاربر ${userId} ناموفق بود: ${error.message}`, {
      cause: error, // نگه‌داشتن ارجاع به خطای اصلی
    });
  }
}

ویژگی دوم سازنده‌ی Error یعنی { cause: error }، امکان زنجیره‌کردن خطاها را فراهم می‌کند: خطای جدید پیام واضح‌تری برای همان لایه دارد، اما از طریق error.cause همچنان می‌توان به خطای اصلی و دقیق‌تر دسترسی داشت.

نکات کلیدی

  • Promise reject‌شده‌ی بدون catch، به‌طور خاموش گم نمی‌شود و رویداد unhandledrejection را در محیط اجرا فعال می‌کند.
  • شنیدن رویداد unhandledrejection روی window برای مانیتورینگ متمرکز خطا کاربرد دارد، نه جایگزین try/catch مناسب.
  • موتورهای مدرن جاوااسکریپت، مسیر stack trace را برای توابع async/await تا حد زیادی حفظ می‌کنند.
  • گزینه‌ی cause در سازنده‌ی Error امکان زنجیره‌کردن خطاها را فراهم می‌کند، بدون از دست دادن اطلاعات خطای اصلی.

جمع‌بندی

مدیریت درست خطا در کد ناهمزمان، فراتر از نوشتن یک try/catch ساده است و شامل آگاهی از خطر Promiseهای بدون مدیریت، استفاده‌ی هدفمند از رویداد unhandledrejection برای مانیتورینگ، و بهره‌گیری از قابلیت‌هایی مثل گزینه‌ی cause برای حفظ زمینه‌ی کامل خطا در سیستم‌های چندلایه است. این مجموعه مفاهیم، فصل خطاها و مدیریت آن‌ها در جاوااسکریپت را تکمیل می‌کند.