تاريخ النشر:

كيفية معالجة الأخطاء المخصصة في تطبيقات Next.js

كتب بواسطة:
  • اسم الكاتب
    تويتر الكاتب

بسم الله والحمد لله والصلاة والسلام على سيدنا محمد رسول الله وبعد،

مؤخرًا أثناء عملي على مهمة شيقة في مساق، استخدمت طريقة عادةً ما أستخدمها لمعالجة الأخطاء المخصصة داخل تطبيقات nextjs. فقررت أن أشارك هذه التجربة وأكتب عنها.

في كثير من الأحيان، عندما يحدث خطأ في تطبيق يعمل بواسطة nextjs، يتم عرض صفحة الخطأ 500 أو صفحة خطأ مخصصة في حال وجود ملف error.tsx. ولكن في بعض الأحيان، يكون هناك حاجة لعرض رسالة خطأ مخصصة أكثر تدل على سبب الخطأ. لحسن الحظ، هناك حيلة بسيطة نوعاً ما يمكننا استخدامها لعرض رسالة خطأ مخصصة بدلاً من صفحة الخطأ الافتراضية.

أولاً، لنأخذ نظرة أوسع حول الموضوع. في مساق، في حال انتهاء اشتراك إحدى المنصات، أي محاولة تواصل مع الـ API تعود بخطأ 402. والمطلوب عرض رسالة خطأ مخصصة تدل على انتهاء الاشتراك عندما يقوم المستخدم بزيارة أي صفحة خاصة بالمنصة من دون تحويله إلى صفحة جديدة مثل /subscription-expired.

لقد قمت بتجربة العديد من الحلول لحل هذه المشكلة، ولكن كلها انتهت بأن الكود أصبح متشعبًا أو تم تطبيق حل لا علاقة له بما أريده. على سبيل المثال:

export default async function RootLayout({ children }) {
  const tenant = await fetchTenant();

  //Handle subscription expired error
  if ("error" in tenant && tenant.error.status === 402) {
    return <SubscriptionExpired />;
  }

  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

هذا الحل لا يعمل بشكل جيد، لأنني بحاجة إلى معالجة نفس الخطأ في كل مرة يتم التواصل فيها مع الـ API وفي كل صفحة! وفي بعض الأحيان، يكون هناك أكثر من طلب للـ API. على سبيل المثال:

export default async function CourseLayout({ children }) {
  const tenant = await fetchTenant();
  const course = await fetchCourse({ ... });
  const content = await fetchContent({ ... });

  return <>children</>;
}

في هذه الحالة، يجب عليّ معالجة هذا الخطأ وربما أخطاء أخرى لكل طلب منفصل! وهذا غير منطقي.

هنا عدت إلى التوثيق للقراءة أكثر عن كيفية التعامل مع الأخطاء ومعالجتها، ووجدت أنه في حال كان الخطأ داخلياً يمكنني استخدام throw في المكان نفسه الذي يتم التواصل فيه مع الـ API ولكن مع رسالة خطأ مخصصة:

export async function fetchTenant() {
  const response = await fetchBaseQuery({ ... });

  if ("error" in response) {
    if (response.error.status === 402) {
      throw new Error("SUBSCRIPTION_EXPIRED");
    }
  }

  return { ... };
}

وبعدها يمكنني أن أعالج الخطأ داخل ملف error.tsx بناءً على الرسالة الخاصة بالخطأ:

export default function Error({ error }) {
  if (error.message === "SUBSCRIPTION_EXPIRED") {
    return <SubscriptionExpired />;
  }

  return <ServerError />;
}

إلى حد هذه اللحظة، الأوضاع لطيفة جداً ومشوقة. قمت بتجربة كل أنواع الأخطاء الخاصة بالنظام الداخلي وتأكدت أنها تعمل بشكل لا شك فيه! وفرحت جداً بأنني وجدت الحل المناسب بعد جهد مطول من البحث والتجربة.

ولكن، واجهت الصدمة أنه عندما يتم بناء التطبيق من أجل البرودكشن، فإن reactjs داخلياً لا تقوم بإرسال الأخطاء مع كافة تفاصيلها لأسباب أمنية. فانتابني الفضول حول كيفية عمل ()notFound في nextjs وكيف يتم عرض الصفحة الخاصة بالخطأ 404 دون تحويل المستخدم إلى صفحة أخرى. عند مراجعة الكود المصدري، وجدت أنه لا يتم الاعتماد على رسالة الخطأ، ولكن يتم الاعتماد على الـ digest الخاص بالخطأ الذي توفره nextjs:

function notFound() {
  const error = new Error("NEXT_NOT_FOUND");
  error.digest = "NEXT_NOT_FOUND";

  throw error;
}

الحل

بعد اكتشاف أن nextjs تعتمد على الـ digest الخاص بالخطأ، فلماذا لا أعتمد عليه أيضاً في التطبيق الذي أعمل عليه؟

export async function fetchTenant() {
  const response = await fetchBaseQuery({...});

  if ("error" in response) {
    if (response.error.status === 402) {
      const error = new Error("SUBSCRIPTION_EXPIRED");
      error.digest = "SUBSCRIPTION_EXPIRED";

      throw error;
    }
  }

  return {...};
}

وبعد تعديل الكود الخاص بالخطأ داخل الملف error.tsx:

export default function Error({ error }) {
  if (error.digest === "SUBSCRIPTION_EXPIRED") {
    return <SubscriptionExpired />;
  }

  return <ServerError />;
}

بوووووم! 🎉

كل شيء يعمل كما هو متوقع دون أي مشاكل، وباستخدام هذه الطريقة البسيطة يمكننا عرض رسالة خطأ مخصصة بدلاً من صفحة الخطأ الافتراضية مع المحافظة على العرف الذي توصي به nextjs.

النهاية

في النهاية، الاعتماد على الـ digest الخاص بالخطأ يعتبر حلاً بسيطاً وأمراً شائعاً جداً، ولكن في بعض الأحيان لا يخطر على البال. لذا، كان من الجيد التحدث عنه قليلاً.

وأنت، ماذا تعتقد؟ هل تستخدم طريقة مختلفة؟ أو لديك حل أفضل من أجل التعامل مع حالات مشابهة؟ تواصل معي على X وأخبرني عن تجربتك ورأيك!