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

در درس‌های قبل این فصل، دو مسیر جدا برای خطا دیدیم: Error Boundary برای خطاهای Render، و مدیریت دستی برای خطاهای Async. در این درس آخر فصل، این دو را در قالب یک استراتژی کامل‌تر برای یک برنامه واقعی کنار هم می‌گذاریم: کجا باید مرز بگذاریم و چند لایه از این مرزها معمولاً لازم است.

لایه اول: یک Error Boundary در ریشه برنامه

حداقل یک Error Boundary، دور کل برنامه، یک تور ایمنی پایه است؛ تا حتی اگر همه راه‌های دیگر شکست بخورند، کاربر یک صفحه سفید خالی نبیند، بلکه یک پیام عمومی (مثلاً «مشکلی پیش آمد، صفحه را تازه کنید») ببیند:

jsx
function App() {
  return (
    <ErrorBoundary fallback={<FullPageError />}>
      <Router />
    </ErrorBoundary>
  );
}

لایه دوم: Error Boundary دور هر صفحه (Route)

همان‌طور که در فصل پانزدهم با Nested Routes دیدیم، هر صفحه معمولاً داخل یک Layout مشترک رندر می‌شود. قرار دادن یک Error Boundary دور هر صفحه، باعث می‌شود اگر یک صفحه خاص خطا بدهد، Header، Footer و نوار ناوبری (که بیرون از این مرز هستند) همچنان کار کنند و کاربر بتواند به صفحه دیگری برود:

jsx
{
  path: "products/:id",
  element: (
    <ErrorBoundary fallback={<p>این صفحه در دسترس نیست.</p>}>
      <ProductPage />
    </ErrorBoundary>
  ),
}

لایه سوم: Error Boundary دور ویجت‌های مستقل و پرریسک

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

خطاهای Async، مستقل از این لایه‌ها مدیریت می‌شوند

مهم است به یاد داشته باشیم که این لایه‌بندی Error Boundary، فقط خطاهای Render را پوشش می‌دهد. خطاهای Async (دریافت داده، Event Handlerها)، طبق چیزی که در درس قبل دیدیم، همچنان باید در همان کامپوننتی که رخ می‌دهند، با State خطای محلی مدیریت شوند؛ مگر اینکه عمداً با همان ترفند پرتاب دوباره، به یکی از این مرزها متصل شوند.

یک نقشه ذهنی ساده برای تصمیم‌گیری

سؤال پاسخ
اگر این بخش خراب شود، آیا کل برنامه باید غیرقابل‌استفاده شود؟ اگر نه، یک Error Boundary اختصاصی دور همین بخش بگذارید.
آیا این خطا در Render رخ می‌دهد یا در یک Callback/Event؟ Render → Error Boundary؛ Callback/Event → مدیریت دستی با state خطا.
آیا یک تور ایمنی کلی در ریشه برنامه وجود دارد؟ باید همیشه حداقل یکی وجود داشته باشد.

گزارش خطا، فارغ از این‌که کجا گرفته شده

چه خطا در یک Error Boundary (با componentDidCatch، که در درس سوم دیدیم) گرفته شود، چه در یک catch دستی برای خطای Async، منطقی است که در هر دو مسیر، همان تابع مشترک گزارش خطا به یک سرویس بیرونی فراخوانی شود؛ تا تیم توسعه، بدون وابستگی به این‌که کاربر خودش گزارش دهد، از وقوع مشکل باخبر شود.

مثال قابل اجرا

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

دو لایه Error Boundary مستقل در کنار هم
import { Component, useState } from "react";

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}

function RiskyWidget({ broken }) {
  if (broken) throw new Error("خطای ویجت");
  return <p>ویجت سالم است.</p>;
}

function App() {
  const [broken, setBroken] = useState(false);

  return (
    <ErrorBoundary fallback={<p>کل برنامه با مشکل مواجه شد.</p>}>
      <div>
        <p>این بخش همیشه باید سالم بماند.</p>
        <button onClick={() => setBroken(true)}>خراب کردن ویجت</button>
        <ErrorBoundary fallback={<p>فقط این ویجت خراب شد.</p>}>
          <RiskyWidget broken={broken} />
        </ErrorBoundary>
      </div>
    </ErrorBoundary>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

یک استراتژی کامل مدیریت خطا، معمولاً چند لایه Error Boundary دارد: یک مرز کلی در ریشه برنامه، مرزهایی دور هر صفحه، و مرزهای کوچک‌تر دور ویجت‌های پرریسک؛ در کنار مدیریت جداگانه و دستی خطاهای Async که هیچ‌کدام از این مرزها آن‌ها را نمی‌گیرند. با این درس، فصل هفدهم به پایان می‌رسد و در ادامه به آزمون فصل ۱۷ می‌رویم.