مدیریت خطاهای Async: جایی که Error Boundary کافی نیست

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

چرا خطای Async، در جریان Render نیست

وقتی یک fetch داخل یک Event Handler یا یک useEffect شکست می‌خورد، این خطا در یک Callback جداگانه، بعد از اینکه React کارش با Render تمام شده، رخ می‌دهد. React هیچ راهی ندارد که این خطا را به یک کامپوننت یا به Error Boundary والدش ربط دهد؛ چون از دید React، Render آن کامپوننت مدت‌ها پیش، بدون مشکل، تمام شده بود.

jsx
function UserProfile({ userId }) {
  useEffect(() => {
    fetchUser(userId).then((data) => {
      // اگر اینجا خطا رخ دهد، هیچ Error Boundary آن را نمی‌گیرد
      setUser(data);
    });
  }, [userId]);
}

راه‌حل: مدیریت دستی با try/catch یا catch

همان‌طور که در فصل چهاردهم با جزئیات دیدیم، درست‌ترین راه برای خطاهای دریافت داده، گرفتن آن‌ها مستقیماً با .catch() یا try/catch و ذخیره در یک State مخصوص خطاست:

jsx
function UserProfile({ userId }) {
  const [error, setError] = useState(null);

  useEffect(() => {
    setError(null);
    fetchUser(userId)
      .then((data) => setUser(data))
      .catch((err) => setError(err.message));
  }, [userId]);

  if (error) return <p>خطا: {error}</p>;
  // ...
}

این دقیقاً همان الگویی است که در فصل چهاردهم دیدیم؛ تفاوتی که این درس اضافه می‌کند، این است که دلیل اینکه چرا باید این کار را دستی انجام دهیم (و نمی‌توانیم به Error Boundary تکیه کنیم) را روشن می‌کند.

خطای داخل یک Event Handler

مشابه همین وضعیت، برای خطایی که مستقیماً و به‌صورت همزمان داخل یک Event Handler رخ می‌دهد (نه در Render)، باید از try/catch معمولی جاوااسکریپت استفاده کرد:

jsx
function SubmitButton() {
  const [error, setError] = useState(null);

  function handleClick() {
    try {
      riskyOperation();
    } catch (err) {
      setError(err.message);
    }
  }

  return (
    <div>
      <button onClick={handleClick}>ارسال</button>
      {error && <p>خطا: {error}</p>}
    </div>
  );
}

یک راه برای وصل کردن خطای Async به یک Error Boundary

اگر واقعاً می‌خواهید یک خطای Async، همان رفتار Error Boundary (یعنی Fallback کامل به‌جای یک پیام کوچک) را داشته باشد، یک ترفند رایج این است که خطا را داخل State ذخیره کنید و سپس آن را دوباره، این‌بار در حین Render، پرتاب کنید:

jsx
function UserProfile({ userId }) {
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchUser(userId).catch((err) => setError(err));
  }, [userId]);

  if (error) {
    throw error; // حالا در حین Render پرتاب می‌شود و Error Boundary آن را می‌گیرد
  }
  // ...
}

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

کدام رویکرد را انتخاب کنیم

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

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

مدیریت یک خطای Async با State جداگانه
import { useState, useEffect } from "react";

function fakeFetchUser() {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error("سرور پاسخ نداد")), 800);
  });
}

function UserProfile() {
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fakeFetchUser()
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <p>در حال بارگذاری...</p>;
  if (error) return <p>خطا: {error}</p>;
  return <p>کاربر بارگذاری شد.</p>;
}

function App() {
  return (
    <div>
      <p>بقیه صفحه سالم باقی می‌ماند.</p>
      <UserProfile />
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

خطاهایی که داخل یک Event Handler یا یک عملیات غیرهمزمان (مثل fetch داخل useEffect) رخ می‌دهند، در جریان Render نیستند و Error Boundary آن‌ها را نمی‌گیرد؛ باید با try/catch یا .catch() دستی مدیریت و معمولاً در یک State خطا ذخیره شوند. در صورت نیاز، می‌توان با پرتاب دوباره خطا در حین Render، آن را به مکانیزم Error Boundary هم متصل کرد. در درس بعدی، به مدیریت خطا در مرزهای مختلف یک برنامه بزرگ‌تر می‌پردازیم.