مدیریت خطا در React: از componentDidCatch تا امروز

مدیریت خطا در React: از componentDidCatch تا امروز
در این مقاله می‌خوانید
  1. سه دستهٔ خطا در React
  2. ۱. خطای رندر
  3. ۲. خطای رویداد
  4. ۳. خطای ناهمگام
  5. مرز خطا برای دستهٔ اول
  6. کجا بگذاریمش
  7. دستهٔ دوم: خطای رویداد
  8. دستهٔ سوم: خطای ناهمگام
  9. در Next.js فرق می‌کند
  10. کتابخانه‌های دریافت داده
  11. سه حالت، نه دو حالت
  12. در production
  13. جمع‌بندی
  14. منابع و مطالعهٔ بیشتر

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

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

سه دستهٔ خطا در React

تفکیک این سه، کلید فهمیدن بقیهٔ مقاله است، چون هر کدام مکانیزم متفاوتی می‌خواهند.

۱. خطای رندر

حین ساختن رابط کاربری رخ می‌دهد و همان چیزی است که صفحهٔ سفید می‌سازد. رایج‌ترین علتش Cannot read properties of undefined است.

function Profile({ user }) {
  return <h1>{user.name}</h1>;   // اگر user نباشد، کل درخت می‌رود
}

۲. خطای رویداد

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

۳. خطای ناهمگام

در useEffect، در fetch، یا در تایمر. این‌ها هم از دست مرز خطا در می‌روند و معمولاً بی‌صداترین دسته‌اند. جزئیاتش را در Unhandled Promise Rejection ببینید.

مرز خطا برای دستهٔ اول

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

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };        // رابط جایگزین را نشان بده
  }

  componentDidCatch(error, info) {
    reportError(error, info.componentStack);   // و خبرش کن
  }

  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}

دو متد، دو کار متفاوت: getDerivedStateFromError تصمیم می‌گیرد چه نشان داده شود، و componentDidCatch جایی است که خطا را ثبت می‌کنید. اگر دومی را ننویسید، خطا بی‌صدا پنهان می‌شود — که از صفحهٔ سفید هم بدتر است.

کجا بگذاریمش

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

الگوی مؤثر، چند لایه است:

<ErrorBoundary fallback={<FullPageError />}>        {/* آخرین خط دفاع */}
  <Layout>
    <ErrorBoundary fallback={<p>نمایش نظرات ممکن نشد</p>}>
      <Comments />                                  {/* فقط این بخش می‌رود */}
    </ErrorBoundary>
  </Layout>
</ErrorBoundary>

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

دستهٔ دوم: خطای رویداد

چون مرز خطا این‌ها را نمی‌گیرد، باید صریح مدیریتشان کنید:

async function handleSubmit() {
  setStatus('loading');
  try {
    const res = await api.save(form);
    if (!res.ok) throw new Error('HTTP ' + res.status);
    setStatus('done');
  } catch (e) {
    setStatus('error');     // به کاربر بگو
    reportError(e);         // و خودت هم بدان
  }
}

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

دستهٔ سوم: خطای ناهمگام

مورد کلاسیک، useEffectای است که داده می‌گیرد:

useEffect(() => {
  let cancelled = false;
  (async () => {
    try {
      const res = await fetch('/api/user');
      if (!res.ok) throw new Error('HTTP ' + res.status);
      const data = await res.json();
      if (!cancelled) setUser(data);
    } catch (e) {
      if (!cancelled) { setError(e); reportError(e); }
    }
  })();
  return () => { cancelled = true; };   // از به‌روزرسانی پس از unmount جلوگیری کن
}, []);

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

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

اگر از Next.js استفاده می‌کنید، بخشی از کد روی سرور اجرا می‌شود و مرزهای خطای React آنجا کار نمی‌کنند. Next الگوی خودش را دارد — فایل‌های ویژه‌ای که به‌عنوان مرز عمل می‌کنند:

// app/dashboard/error.jsx
'use client';                       // مرز خطا حتماً کلاینتی است

export default function Error({ error, reset }) {
  useEffect(() => { reportError(error); }, [error]);
  return (
    <div>
      <p>بارگذاری این بخش ممکن نشد.</p>
      <button onClick={reset}>تلاش دوباره</button>
    </div>
  );
}

این فایل به‌طور خودکار مرز خطای همان مسیر و زیرمسیرهایش می‌شود. نکتهٔ مهم: خطای خود layout را نمی‌گیرد — برای آن به فایلی در سطح بالاتر نیاز دارید.

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

کتابخانه‌های دریافت داده

اگر از React Query یا SWR استفاده می‌کنید، بخش بزرگی از مدیریت خطای ناهمگام را خودشان انجام می‌دهند و کد شما ساده‌تر می‌شود:

const { data, error, isLoading, refetch } = useQuery({
  queryKey: ['user', id],
  queryFn: fetchUser,
});

if (isLoading) return <Spinner />;
if (error)     return <Retry onClick={refetch} />;
return <Profile user={data} />;

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

سه حالت، نه دو حالت

رایج‌ترین ریشهٔ خطای رندر در React این است که فقط دو حالت در نظر گرفته می‌شود: داده هست یا نیست. در واقع سه حالت وجود دارد:

if (loading) return <Spinner />;
if (error)   return <Retry onClick={refetch} />;
return <Profile user={user} />;

اگر حالت میانی را ننویسید، کامپوننت در اولین رندر با دادهٔ خالی اجرا می‌شود و همان خطای Cannot read properties of undefined را می‌گیرید.

در production

سه نکته که بدون آن‌ها، خطاهای React عملاً غیرقابل ردیابی می‌مانند:

پیام‌های خطا فشرده‌اند. React در حالت production پیام‌ها را به شمارهٔ کد تبدیل می‌کند تا حجم کم شود. لینکی که همراهش می‌آید، متن کامل را نشان می‌دهد.

componentStack را ثبت کنید. این جدا از stack معمولی است و می‌گوید خطا در کدام کامپوننت و در کدام سلسله‌مراتب رخ داد — که معمولاً مفیدتر از stack خودِ جاوااسکریپت است.

Source map لازم دارید، وگرنه stack به کد فشرده اشاره می‌کند و چیزی نمی‌گوید.

جمع‌بندی

مدیریت خطا در React سه لایه دارد و هر لایه مکانیزم خودش را می‌خواهد: مرز خطا برای رندر، try/catch برای رویدادها، و مدیریت صریح برای کد ناهمگام. پوشش یکی از این سه، دو تای دیگر را پوشش نمی‌دهد. برای شناخت خود خطاها، راهنمای جامع خطاهای جاوااسکریپت را ببینید.

و لایهٔ چهارمی هم هست که بیرون از React می‌ماند: شنوندهٔ سراسری خطا، برای هر چیزی که از این سه رد شده باشد.

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

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

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

شروع رایگان