معالجة الأخطاء وحدود الأخطاء
مقدمة إلى معالجة الأخطاء
معالجة الأخطاء القوية ضرورية لتطبيقات الإنتاج. يوفر Next.js 13+ نظام معالجة أخطاء شامل باستخدام ملفات خاصة مثل error.tsx و global-error.tsx و not-found.tsx. يغطي هذا الدرس تنفيذ معالجة أخطاء مرنة في جميع أنحاء تطبيقك.
فهم ملفات الأخطاء
يستخدم 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>
)
}
معالج الأخطاء العالمي
أنشئ 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>
)
}
صفحات غير موجودة
أنشئ صفحات 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>
}
- أنشئ صفحات أخطاء مخصصة لأجزاء مسار مختلفة (المدونة، لوحة المعلومات، الملف الشخصي)
- قم بتنفيذ تسجيل الأخطاء في خدمة مثل Sentry أو LogRocket
- أضف منطق إعادة المحاولة مع التراجع الأسي لطلبات API الفاشلة
- أنشئ حد خطأ مخصص يعرض واجهة مستخدم مختلفة بناءً على نوع الخطأ
- قم بتنفيذ التدهور الرشيق للميزات غير الحرجة التي تفشل
- أضف رسائل خطأ سهلة الفهم للسيناريوهات الخطأ الشائعة
أفضل ممارسات معالجة الأخطاء
- قدم دائماً طريقة للتعافي من الأخطاء (زر إعادة المحاولة، روابط التنقل)
- اعرض رسائل خطأ مختلفة في التطوير مقابل الإنتاج
- سجل الأخطاء في خدمات المراقبة لتصحيح الأخطاء
- استخدم صفحات أخطاء محددة لأجزاء مسار مختلفة
- قدم سياقاً مفيداً والخطوات التالية في رسائل الخطأ
- تحقق من الإدخال مبكراً لالتقاط الأخطاء قبل أن تنتشر
- استخدم رموز حالة HTTP المناسبة في استجابات API
- قم بتنفيذ واجهة مستخدم احتياطية للتدهور الرشيق
- لا تكشف أبداً تفاصيل الأخطاء الحساسة للمستخدمين في الإنتاج
الخلاصة
معالجة الأخطاء الفعالة في Next.js تستخدم ملفات error.tsx و global-error.tsx و not-found.tsx لإنشاء تطبيقات مرنة. قدم دائماً آليات استعادة، وسجل الأخطاء بشكل صحيح، واعرض رسائل سهلة الفهم للمستخدم، ونفذ التدهور الرشيق. معالجة الأخطاء الجيدة تحسن تجربة المستخدم وتجعل تصحيح الأخطاء أسهل.