Runtime Errors: وقتی کد در حین اجرا با مشکل مواجه می‌شود

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

Runtime Error چیست

Runtime Error به خطایی گفته می‌شود که در حین اجرای برنامه رخ می‌دهد، نه در زمان نوشتن یا کامپایل کد. برای مثال:

jsx
function UserCard({ user }) {
  return <p>{user.profile.name}</p>;
}

اگر user.profile به هر دلیلی undefined باشد (مثلاً داده هنوز از سرور نرسیده)، خواندن .name از آن، یک خطای واقعی در حین اجرا پرتاب می‌کند: TypeError: Cannot read properties of undefined.

رفتار پیش‌فرض React: Unmount کل درخت

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

چرا React این‌طور طراحی شده است

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

مشکل این رفتار پیش‌فرض برای تجربه کاربری

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

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

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

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

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

مشاهده رفتار پیش‌فرض React در برابر یک خطای Render
import { useState } from "react";

function UserCard({ user }) {
  return <p>نام: {user.profile.name}</p>;
}

function App() {
  const [showBroken, setShowBroken] = useState(false);

  return (
    <div>
      <p>این متن همیشه باید نمایش داده شود.</p>
      <button onClick={() => setShowBroken(true)}>
        نمایش کامپوننت دارای خطا
      </button>
      {showBroken && <UserCard user={{}} />}
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

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