إطار Next.js

معالجة الأخطاء وحدود الأخطاء

24 دقيقة الدرس 20 من 40

مقدمة إلى معالجة الأخطاء

معالجة الأخطاء القوية ضرورية لتطبيقات الإنتاج. يوفر Next.js 13+ نظام معالجة أخطاء شامل باستخدام ملفات خاصة مثل error.tsx و global-error.tsx و not-found.tsx. يغطي هذا الدرس تنفيذ معالجة أخطاء مرنة في جميع أنحاء تطبيقك.

مفهوم أساسي: يستخدم Next.js حدود أخطاء React تحت الغطاء، ويلف تلقائياً أجزاء المسار لالتقاط الأخطاء وتوفير واجهة مستخدم احتياطية دون تعطل التطبيق بأكمله.

فهم ملفات الأخطاء

يستخدم Next.js نظام معالجة أخطاء قائم على الملفات مع اصطلاحات تسمية محددة:

  • error.tsx: يلتقط الأخطاء في جزء المسار والأطفال المتداخلين
  • global-error.tsx: يلتقط الأخطاء في التخطيط الجذري (نادر لكنه حاسم)
  • not-found.tsx: يعرض عندما لا يتم العثور على مسار أو مورد
  • loading.tsx: يظهر أثناء تحميل المحتوى (تم تغطيته في الدروس السابقة)

إنشاء حد خطأ

أنشئ ملف error.tsx لالتقاط الأخطاء داخل جزء المسار:

<!-- app/error.tsx -->
'use client'

import { useEffect } from 'react'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // تسجيل الخطأ في خدمة تقارير الأخطاء
    console.error('حد الخطأ التقط:', error)
  }, [error])

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
      <div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center">
        <div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
          <svg
            className="w-8 h-8 text-red-600"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={2}
              d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
            />
          </svg>
        </div>

        <h2 className="text-2xl font-bold text-gray-900 mb-2">
          حدث خطأ ما!
        </h2>

        <p className="text-gray-600 mb-6">
          نعتذر عن الإزعاج. حدث خطأ أثناء معالجة طلبك.
        </p>

        {process.env.NODE_ENV === 'development' && (
          <div className="mb-6 p-4 bg-gray-100 rounded-lg text-left">
            <p className="text-sm font-mono text-red-600 break-all">
              {error.message}
            </p>
            {error.digest && (
              <p className="text-xs text-gray-500 mt-2">
                معرف الخطأ: {error.digest}
              </p>
            )}
          </div>
        )}

        <button
          onClick={reset}
          className="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
        >
          حاول مرة أخرى
        </button>

        <a
          href="/"
          className="block mt-4 text-sm text-blue-600 hover:text-blue-700"
        >
          العودة إلى الصفحة الرئيسية
        </a>
      </div>
    </div>
  )
}
مهم: يجب أن يكون error.tsx مكون عميل ('use client') لأنه يستخدم خطافات React مثل useEffect. تسمح دالة reset للمستخدمين بإعادة محاولة العملية.

معالج الأخطاء العالمي

أنشئ global-error.tsx لالتقاط الأخطاء في التخطيط الجذري:

<!-- app/global-error.tsx -->
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <div className="min-h-screen flex items-center justify-center bg-gray-900 text-white px-4">
          <div className="max-w-md w-full text-center">
            <h1 className="text-4xl font-bold mb-4">خطأ حرج</h1>
            <p className="text-gray-400 mb-8">
              حدث خطأ حرج. يرجى تحديث الصفحة أو الاتصال بالدعم.
            </p>

            {process.env.NODE_ENV === 'development' && (
              <div className="mb-6 p-4 bg-red-900 rounded-lg text-left">
                <p className="text-sm font-mono break-all">
                  {error.message}
                </p>
              </div>
            )}

            <button
              onClick={reset}
              className="px-6 py-3 bg-white text-gray-900 rounded-lg hover:bg-gray-100"
            >
              حاول مرة أخرى
            </button>
          </div>
        </div>
      </body>
    </html>
  )
}
ملاحظة: يجب أن يتضمن global-error.tsx علامات <html> و <body> لأنه يستبدل التخطيط الجذري عند التنشيط. نادراً ما يتم تشغيله لكنه حاسم لالتقاط الأخطاء في app/layout.tsx.

صفحات غير موجودة

أنشئ صفحات 404 مخصصة باستخدام not-found.tsx:

<!-- app/not-found.tsx -->
import Link from 'next/link'

export default function NotFound() {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 px-4">
      <div className="max-w-lg w-full text-center">
        <h1 className="text-9xl font-bold text-blue-600 mb-4">404</h1>
        <h2 className="text-3xl font-bold text-gray-900 mb-4">
          الصفحة غير موجودة
        </h2>
        <p className="text-gray-600 mb-8">
          عذراً، لم نتمكن من العثور على الصفحة التي تبحث عنها.
          ربما تم نقلها أو حذفها.
        </p>

        <div className="flex flex-col sm:flex-row gap-4 justify-center">
          <Link
            href="/"
            className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
          >
            الذهاب إلى الصفحة الرئيسية
          </Link>
          <button
            onClick={() => window.history.back()}
            className="px-6 py-3 bg-white text-gray-900 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
          >
            الرجوع
          </button>
        </div>

        <div className="mt-12">
          <p className="text-sm text-gray-500 mb-4">الصفحات الشائعة:</p>
          <div className="flex flex-wrap justify-center gap-2">
            <Link href="/about" className="text-sm text-blue-600 hover:underline">
              حول
            </Link>
            <span className="text-gray-300">•</span>
            <Link href="/blog" className="text-sm text-blue-600 hover:underline">
              المدونة
            </Link>
            <span className="text-gray-300">•</span>
            <Link href="/contact" className="text-sm text-blue-600 hover:underline">
              اتصل بنا
            </Link>
          </div>
        </div>
      </div>
    </div>
  )
}

غير موجود برمجياً

قم بتشغيل صفحة غير موجودة برمجياً باستخدام notFound():

<!-- app/posts/[slug]/page.tsx -->
import { notFound } from 'next/navigation'
import { prisma } from '@/lib/prisma'

interface PageProps {
  params: {
    slug: string
  }
}

export default async function PostPage({ params }: PageProps) {
  const post = await prisma.post.findUnique({
    where: { slug: params.slug }
  })

  if (!post) {
    notFound()
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

صفحات أخطاء خاصة بالمسار

أنشئ صفحات أخطاء لأجزاء مسار محددة:

<!-- app/dashboard/error.tsx -->
'use client'

import { useRouter } from 'next/navigation'

export default function DashboardError({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  const router = useRouter()

  return (
    <div className="p-8 max-w-lg mx-auto">
      <h2 className="text-2xl font-bold text-red-600 mb-4">
        خطأ في لوحة المعلومات
      </h2>
      <p className="text-gray-600 mb-6">
        حدث خطأ أثناء تحميل لوحة المعلومات الخاصة بك. قد يكون هذا بسبب
        مشكلة في الشبكة أو مشكلة مؤقتة في خوادمنا.
      </p>

      <div className="flex gap-4">
        <button
          onClick={reset}
          className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
        >
          إعادة المحاولة
        </button>
        <button
          onClick={() => router.push('/')}
          className="px-4 py-2 bg-gray-200 text-gray-900 rounded-lg hover:bg-gray-300"
        >
          الذهاب إلى الصفحة الرئيسية
        </button>
      </div>
    </div>
  )
}
<!-- app/dashboard/not-found.tsx -->
export default function DashboardNotFound() {
  return (
    <div className="p-8 text-center">
      <h2 className="text-2xl font-bold mb-4">صفحة لوحة المعلومات غير موجودة</h2>
      <p className="text-gray-600">
        صفحة لوحة المعلومات التي تبحث عنها غير موجودة.
      </p>
    </div>
  )
}

أنماط استعادة الأخطاء

قم بتنفيذ استراتيجيات استعادة أخطاء ذكية:

<!-- app/components/error-boundary-wrapper.tsx -->
'use client'

import { Component, ReactNode } from 'react'

interface Props {
  children: ReactNode
  fallback: (error: Error, retry: () => void) => ReactNode
}

interface State {
  hasError: boolean
  error: Error | null
  errorCount: number
}

export class ErrorBoundaryWrapper extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = {
      hasError: false,
      error: null,
      errorCount: 0
    }
  }

  static getDerivedStateFromError(error: Error): Partial<State> {
    return {
      hasError: true,
      error,
    }
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('حد الخطأ التقط:', error, errorInfo)

    // إرسال إلى خدمة تقارير الأخطاء
    // reportError(error, errorInfo)

    this.setState(prevState => ({
      errorCount: prevState.errorCount + 1
    }))
  }

  retry = () => {
    // تحديد محاولات إعادة المحاولة
    if (this.state.errorCount < 3) {
      this.setState({
        hasError: false,
        error: null,
      })
    } else {
      alert('تم الوصول إلى الحد الأقصى لمحاولات إعادة المحاولة. يرجى تحديث الصفحة.')
    }
  }

  render() {
    if (this.state.hasError && this.state.error) {
      return this.props.fallback(this.state.error, this.retry)
    }

    return this.props.children
  }
}

معالجة أخطاء API

معالجة الأخطاء في مسارات API وإجراءات الخادم:

<!-- app/api/posts/route.ts -->
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'

const createPostSchema = z.object({
  title: z.string().min(1),
  content: z.string().min(1),
})

export async function POST(request: Request) {
  try {
    const body = await request.json()

    // التحقق من الإدخال
    const validatedData = createPostSchema.parse(body)

    // إنشاء المنشور
    const post = await prisma.post.create({
      data: validatedData
    })

    return NextResponse.json(post, { status: 201 })

  } catch (error) {
    console.error('خطأ في إنشاء المنشور:', error)

    // خطأ في التحقق من الصحة
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        {
          error: 'فشل التحقق من الصحة',
          issues: error.issues
        },
        { status: 400 }
      )
    }

    // خطأ في قاعدة البيانات
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      if (error.code === 'P2002') {
        return NextResponse.json(
          { error: 'منشور بهذا العنوان موجود بالفعل' },
          { status: 409 }
        )
      }
    }

    // خطأ عام
    return NextResponse.json(
      { error: 'خطأ داخلي في الخادم' },
      { status: 500 }
    )
  }
}

معالجة أخطاء إجراءات الخادم

<!-- app/actions/post-actions.ts -->
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'

const postSchema = z.object({
  title: z.string().min(1, 'العنوان مطلوب'),
  content: z.string().min(10, 'يجب أن يكون المحتوى 10 أحرف على الأقل'),
})

export async function createPost(prevState: any, formData: FormData) {
  try {
    const rawData = {
      title: formData.get('title'),
      content: formData.get('content'),
    }

    // التحقق من الصحة
    const validatedData = postSchema.parse(rawData)

    // إنشاء المنشور
    const post = await prisma.post.create({
      data: validatedData
    })

    // إعادة التحقق وإعادة التوجيه
    revalidatePath('/posts')
    redirect(`/posts/${post.slug}`)

  } catch (error) {
    console.error('خطأ في إنشاء المنشور:', error)

    if (error instanceof z.ZodError) {
      return {
        success: false,
        errors: error.flatten().fieldErrors,
        message: 'فشل التحقق من الصحة'
      }
    }

    return {
      success: false,
      message: 'فشل إنشاء المنشور. يرجى المحاولة مرة أخرى.'
    }
  }
}

معالجة الأخطاء من جانب العميل

<!-- app/components/safe-component.tsx -->
'use client'

import { useState, useEffect } from 'react'

export function SafeComponent() {
  const [data, setData] = useState(null)
  const [error, setError] = useState<string | null>(null)
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    fetchData()
  }, [])

  const fetchData = async () => {
    try {
      setIsLoading(true)
      setError(null)

      const response = await fetch('/api/data')

      if (!response.ok) {
        throw new Error(`خطأ HTTP! الحالة: ${response.status}`)
      }

      const result = await response.json()
      setData(result)

    } catch (err) {
      console.error('خطأ في الجلب:', err)
      setError(err instanceof Error ? err.message : 'حدث خطأ')
    } finally {
      setIsLoading(false)
    }
  }

  if (isLoading) {
    return <div>جاري التحميل...</div>
  }

  if (error) {
    return (
      <div className="p-4 bg-red-50 border border-red-200 rounded-lg">
        <h3 className="text-red-800 font-semibold mb-2">خطأ</h3>
        <p className="text-red-600 mb-4">{error}</p>
        <button
          onClick={fetchData}
          className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
        >
          إعادة المحاولة
        </button>
      </div>
    )
  }

  return <div>{/* عرض البيانات */}</div>
}
تمرين عملي:
  1. أنشئ صفحات أخطاء مخصصة لأجزاء مسار مختلفة (المدونة، لوحة المعلومات، الملف الشخصي)
  2. قم بتنفيذ تسجيل الأخطاء في خدمة مثل Sentry أو LogRocket
  3. أضف منطق إعادة المحاولة مع التراجع الأسي لطلبات API الفاشلة
  4. أنشئ حد خطأ مخصص يعرض واجهة مستخدم مختلفة بناءً على نوع الخطأ
  5. قم بتنفيذ التدهور الرشيق للميزات غير الحرجة التي تفشل
  6. أضف رسائل خطأ سهلة الفهم للسيناريوهات الخطأ الشائعة

أفضل ممارسات معالجة الأخطاء

  • قدم دائماً طريقة للتعافي من الأخطاء (زر إعادة المحاولة، روابط التنقل)
  • اعرض رسائل خطأ مختلفة في التطوير مقابل الإنتاج
  • سجل الأخطاء في خدمات المراقبة لتصحيح الأخطاء
  • استخدم صفحات أخطاء محددة لأجزاء مسار مختلفة
  • قدم سياقاً مفيداً والخطوات التالية في رسائل الخطأ
  • تحقق من الإدخال مبكراً لالتقاط الأخطاء قبل أن تنتشر
  • استخدم رموز حالة HTTP المناسبة في استجابات API
  • قم بتنفيذ واجهة مستخدم احتياطية للتدهور الرشيق
  • لا تكشف أبداً تفاصيل الأخطاء الحساسة للمستخدمين في الإنتاج

الخلاصة

معالجة الأخطاء الفعالة في Next.js تستخدم ملفات error.tsx و global-error.tsx و not-found.tsx لإنشاء تطبيقات مرنة. قدم دائماً آليات استعادة، وسجل الأخطاء بشكل صحيح، واعرض رسائل سهلة الفهم للمستخدم، ونفذ التدهور الرشيق. معالجة الأخطاء الجيدة تحسن تجربة المستخدم وتجعل تصحيح الأخطاء أسهل.