پرتوپرتو

الگوهای خطا

راهنمای نمایش خطاها در پرتو — API errors، validation، و حالت‌های سیستمی

انواع خطا

نوعتوضیحالگو
خطای APIدرخواست به سرور شکست خوردErrorState یا Alert
خطای validationورودی کاربر نامعتبر استFormMessage زیر فیلد
خطای سطح صفحهصفحه قابل نمایش نیستerror.tsx در Next.js
خطای شبکهاتصال اینترنت نیستAlert با دکمه retry
خطای مجوزدسترسی غیرمجازصفحه 403 یا Alert

نمونه بصری — حالت خطای API


ErrorState کامپوننت

برای خطاهایی که کل صفحه یا بخش اصلی را تحت تأثیر می‌گذارند:

;<
  ="اطلاعات اینفلوئنسرها در حال حاضر قابل دسترسی نیست. لطفاً دوباره تلاش کنید."
  ={}
/>

دکمه تلاش مجدد را خودِ کامپوننت می‌سازد

ErrorState سه prop title/description/action ندارد. کل رابطش message، onRetry، retryLabel، size و locale است. با دادن onRetry دکمه‌ی تلاش مجدد خودکار ظاهر می‌شود؛ برای عوض‌کردن متنش از retryLabel استفاده کنید.


Alert برای خطاهای inline

برای خطاهایی که در میان محتوا نمایش داده می‌شوند:

import { Alert, AlertTitle, AlertDescription } from '@partodata/ui'

{
  error && (
    <Alert variant="destructive">
      <AlertCircle className="h-4 w-4" />
      <AlertTitle>عملیات ناموفق بود</AlertTitle>
      <AlertDescription>{error.message || 'خطایی رخ داده است. لطفاً دوباره تلاش کنید.'}</AlertDescription>
    </Alert>
  )
}

خطای API در داشبورد

function DashboardSection() {
  const { data, error, isLoading, refetch } = useQuery(...)

  if (isLoading) return <SectionSkeleton />

  if (error) return (
    <div className="bg-surface-100 border border-default rounded-lg p-6 flex flex-col items-center gap-4 text-center">
      <AlertCircle className="h-8 w-8 text-destructive" />
      <div>
        <h3 className="text-foreground font-medium">خطا در بارگذاری</h3>
        <p className="text-light text-sm mt-1">داده‌ها در دسترس نیست</p>
      </div>
      <Button variant="outline" size="sm" onClick={refetch}>
        تلاش مجدد
      </Button>
    </div>
  )

  return <DashboardContent data={data} />
}

خطای شبکه

{
  isNetworkError && (
    <Alert>
      <WifiOff className="h-4 w-4" />
      <AlertTitle>اتصال به اینترنت برقرار نیست</AlertTitle>
      <AlertDescription>لطفاً اتصال اینترنت خود را بررسی کنید و دوباره تلاش کنید.</AlertDescription>
    </Alert>
  )
}

error.tsx در Next.js

برای خطاهای سطح صفحه از error.tsx استفاده کنید:

// app/(dashboard)/error.tsx
'use client'

import {  } from 'next/navigation'
import {  } from '@partodata/ui'
import {  } from '@partodata/ui'

export default function ({ ,  }: { : Error; : () => void }) {
  const  = ()

  return (
    < ="flex min-h-[400px] items-center justify-center">
      <
        ="این بخش در حال حاضر در دسترس نیست. تیم فنی در حال بررسی است."
        ={}
        ="lg"
      />
      < ="outline" ={() => .('/')}>
        برگشت به صفحه اصلی
      </>
    </>
  )
}

`router` را import کنید

نسخهٔ پیشین این قطعه router.push() را بدون useRouter صدا می‌زد — یک ReferenceError در همان کلیک. در یک Client Component، useRouter را از next/navigation بگیرید.


پیام‌های خطا — اصول نوشتاری

اشتباهدرست
«خطای ۵۰۰»«سرور در حال حاضر پاسخ نمی‌دهد»
«null reference exception»«اطلاعات یافت نشد»
«در صورت ادامه با پشتیبانی تماس بگیرید»«دوباره تلاش کنید. اگر مشکل ادامه داشت، با پشتیبانی تماس بگیرید»
«مجاز نیست»«برای دیدن این بخش، باید وارد حساب کاربری خود شوید»

قوانین:

  • واضح — کاربر بداند چه اتفاقی افتاده
  • قابل اقدام — کاربر بداند چه کاری بکند
  • بدون اصطلاح فنی — error code را در UI نمایش ندهید
  • فارسی رسمی — «تلاش کنید» نه «امتحان کن»

دسترسی‌پذیری

// خطاها باید توسط screen reader اعلام شوند
<div role="alert" aria-live="assertive">
  {error && (
    <Alert variant="destructive">
      <AlertDescription>{error.message}</AlertDescription>
    </Alert>
  )}
</div>

// برای خطاهای فرم
<div role="alert" aria-live="polite">
  <FormMessage />
</div>

بهترین روش‌ها و دام‌های رایج

  • ابتدا سطح خطا را از جدول «انواع خطا» بالای صفحه تعیین کنید، بعد کامپوننت را انتخاب کنید — خطای یک فیلد هرگز Alert سطح صفحه نمی‌شود و خطای کل صفحه هرگز FormMessage نمی‌شود.
  • هر خطای قابل تکرار (API، شبکه) باید دکمه «تلاش مجدد» داشته باشد — خطای بدون راه خروج، بن‌بست است.
  • خطا را همیشه در کنار isLoading مدیریت کنید: اول skeleton، بعد خطا یا داده — هرگز هم‌زمان هر دو را نمایش ندهید.

دام‌های زیر پرتکرارترین اشتباهات نمایش خطا در محصولات پرتو هستند. هر مورد شامل اشتباه، دلیل، و الگوی درست است.

رنگ hardcode برای حالت خطا

اشتباه: ساختن جعبه خطا با رنگ‌های مستقیم Tailwind مانند bg-red-50 و text-red-600 به جای توکن‌های destructive.

چرا اشتباه است: تم پیش‌فرض پرتو تیره است (:root توکن‌های dark را حمل می‌کند). رنگ‌های hardcode با تغییر تم به‌روز نمی‌شوند — پس‌زمینه روشن قرمز در تم تیره از چیدمان بیرون می‌زند و contrast مورد نیاز WCAG را نقض می‌کند. قانون no-hardcoded-colors در ESLint سیستم نیز آن را رد می‌کند.

الگوی درست: از variant="destructive" در Alert یا توکن‌های destructive استفاده کنید — در هر دو تم مقدار درست می‌گیرند:

// ❌ غلط — در تم تیره (پیش‌فرض پرتو) شکسته می‌شود
<div className="bg-red-50 border border-red-200 text-red-600 rounded-lg p-4">
  ارسال کمپین تخفیف فصلی ناموفق بود
</div>

// ✅ درست — توکن‌ها در هر دو تم کار می‌کنند
<Alert variant="destructive">
  <AlertCircle className="h-4 w-4" />
  <AlertTitle>ارسال کمپین ناموفق بود</AlertTitle>
  <AlertDescription>کمپین «تخفیف فصلی» ذخیره نشد. لطفاً دوباره تلاش کنید.</AlertDescription>
</Alert>

جای‌گذاری فیزیکی آیکون و دکمه بستن

اشتباه: چیدن آیکون خطا و دکمه بستن با property‌های فیزیکی: pl-10، left-3، right-3، text-left.

چرا اشتباه است: در صفحه فارسی (RTL) متن از راست شروع می‌شود، اما left-3 آیکون را در سمت چپ نگه می‌دارد — آیکون و متن در دو سمت مخالف می‌افتند و دکمه بستن روی متن می‌نشیند. این دقیقاً همان چیزی است که قانون no-physical-css-properties جلوی آن را می‌گیرد.

الگوی درست: فقط Logical Properties — ps/pe، start/end، text-start:

// ❌ غلط — در RTL آیکون و دکمه بستن جابه‌جا می‌نشینند
<div className="relative pl-10 text-left">
  <AlertCircle className="absolute left-3 top-3 h-4 w-4" />
  <button className="absolute right-3 top-3" aria-label="بستن">×</button>
</div>

// ✅ درست — در RTL و LTR هر دو درست است
<div className="relative ps-10 text-start">
  <AlertCircle className="absolute start-3 top-3 h-4 w-4" />
  <button className="absolute end-3 top-3" aria-label="بستن">×</button>
</div>

جایگزینی کل DataTable با ErrorState هنگام خطای یک صفحه

اشتباه: در جدول سرور-صفحه‌بندی‌شده، به محض شکست fetch یک صفحه، کل DataTable با ErrorState جایگزین می‌شود.

چرا اشتباه است: DataTable سرور-صفحه‌بندی‌شده است و prop خطا ندارد — مدیریت خطا با مصرف‌کننده است. وقتی مثلاً صفحه ۴ شکست می‌خورد و کل جدول حذف می‌شود، کاربر داده‌های صفحه قبلی، شماره صفحه، و زمینه فیلترها را یک‌جا از دست می‌دهد؛ در حالی که فقط یک درخواست شکست خورده است.

الگوی درست: پوسته جدول را نگه دارید، داده صفحه قبلی را حفظ کنید، و خطا را با Alert و دکمه تلاش مجدد بالای جدول نمایش دهید:

// ❌ غلط — شکست یک صفحه، کل جدول و زمینه کاربر را حذف می‌کند
if (error) return <ErrorState title="خطا در بارگذاری" />

// ✅ درست — جدول می‌ماند؛ خطا inline است و تلاش مجدد همان صفحه را می‌گیرد
const { data, error, refetch, isFetching } = useQuery({
  queryKey: ['packaging-feedback', page],
  queryFn: () => fetchFeedback(page),
  placeholderData: (previous) => previous, // داده صفحه قبلی حفظ می‌شود
})

return (
  <div className="flex flex-col gap-4">
    {error && (
      <Alert variant="destructive">
        <AlertCircle className="h-4 w-4" />
        <AlertTitle>بارگذاری این صفحه ناموفق بود</AlertTitle>
        <AlertDescription className="flex items-center gap-2">
          داده‌های صفحه فعلی دریافت نشد.
          <Button variant="outline" size="sm" onClick={() => refetch()}>
            تلاش مجدد
          </Button>
        </AlertDescription>
      </Alert>
    )}
    <DataTable
      columns={columns}
      data={data?.rows ?? []}
      isLoading={isFetching && !data}
      pagination={{
        currentPage: page,
        totalPages: data?.totalPages ?? 1,
        onPageChange: setPage,
      }}
    />
  </div>
)

نمایش متن خام و فنی سرور به کاربر

اشتباه: پاس‌دادن مستقیم پیام فنی سرور به UI — خروجی چیزی مانند «Request failed with status code 502» می‌شود؛ انگلیسی، فنی، و بدون اقدام.

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

الگوی درست: کد خطا را به پیام فارسی رسمی نگاشت کنید و برای موارد ناشناخته fallback عمومی بگذارید؛ متن فنی فقط به لاگ برود. نمایش error.message فقط وقتی مجاز است که سرور پیام فارسی کاربرپسند برگرداند — و حتی در آن حالت هم مانند بخش «Alert برای خطاهای inline» بالای صفحه، fallback فارسی کنار آن بگذارید:

// ❌ غلط — message سرور متن فنی انگلیسی است و مستقیم نمایش داده می‌شود
<AlertDescription>{error.message}</AlertDescription>

// ✅ درست — نگاشت به فارسی رسمی + fallback عمومی
const errorMessages: Record<string, string> = {
  NETWORK: 'اتصال به اینترنت برقرار نیست. لطفاً اتصال خود را بررسی کنید.',
  FORBIDDEN: 'برای دیدن این بخش، باید وارد حساب کاربری خود شوید.',
  NOT_FOUND: 'اطلاعات مورد نظر یافت نشد.',
}

<AlertDescription>
  {errorMessages[error.code] ?? 'خطایی رخ داده است. لطفاً دوباره تلاش کنید.'}
</AlertDescription>

mount شرطی ناحیه aria-live

اشتباه: رندر شرطی ناحیه aria-live همراه با خود خطا — یعنی {error && <div aria-live="assertive">...</div>}.

چرا اشتباه است: screen reader فقط تغییرات یک ناحیه live از قبل موجود را اعلام می‌کند. اگر ناحیه هم‌زمان با خطا mount شود، بسیاری از screen reader‌ها آن را نادیده می‌گیرند و کاربر نابینا متوجه شکست عملیات نمی‌شود. توجه کنید که role="alert" داخلی خود Alert هم به همین دلیل کافی نیست — چون همراه با محتوا mount می‌شود.

الگوی درست: همان الگوی بخش «دسترسی‌پذیری» بالا — wrapper همیشه در DOM باشد و فقط محتوای آن تغییر کند:

// ❌ غلط — ناحیه live همراه با خطا mount می‌شود؛ اعلام نمی‌شود
{error && (
  <div aria-live="assertive">
    <Alert variant="destructive">
      <AlertDescription>ارسال بازخورد بسته‌بندی ناموفق بود</AlertDescription>
    </Alert>
  </div>
)}

// ✅ درست — ناحیه live همیشه هست؛ فقط محتوا تغییر می‌کند
<div role="alert" aria-live="assertive">
  {error && (
    <Alert variant="destructive">
      <AlertDescription>ارسال بازخورد بسته‌بندی ناموفق بود</AlertDescription>
    </Alert>
  )}
</div>

صفحات مرتبط