checklist للاستثناءات غير المتوقّعة - هل يجب أن يخرج التطبيق أم يستمرّ؟ جدول قرار عمليّ

· · Windows Development, Exception Handling, Architecture, C# / .NET, Reliability

تحميل checklist بصيغة Excel مع ورقتي اليابانيّة والإنجليزيّة

عند ظهور استثناء غير متوقّع، كثيراً ما ينهار النقاش بسرعة كبيرة إلى ثنائيّة سطحيّة:

«هل نتركه ينهار، أم نـ catch ونستمرّ؟»

في الأنظمة الواقعيّة، هذه الصياغة عادةً ما تكون فظّة أكثر من اللازم.

السؤال الأكثر فائدةً هو:

هل يمكن احتواء الجزء الذي قد يكون قد تضرّر؟

  • هل هذه مجرّد عمليّة فاشلة فقط؟
  • هل يمكن إعادة تهيئة شاشة واحدة، أو اتّصال واحد، أو worker واحد؟
  • أم أنّ سلامة العمليّة بأكملها أصبحت موضع شكّ الآن؟

هذا التسلسل يقود إلى قرارات أفضل في أغلب الأحيان.

كُتب هذا المقال مع أخذ تطبيقات Windows بـ C# / .NET وأدوات المقيمة (resident) و Windows services وأدوات التكامل مع الأجهزة في الاعتبار. الهدف هو توفير جدول قرار عمليّ لمتى يمكن النجاة من استثناء غير متوقّع ومتى يجب على التطبيق الخروج بدلاً من ذلك.

1. النسخة المختصرة

  • catch (Exception) ومتابعة العمل عادةً ما تكون خطيرة.
  • يكون الاستمرار قابلاً للدفاع عنه فقط عندما يمكن التخلّص من الوحدة الفاشلة، ويمكن إصلاح الحالة المشتركة أو إعادة إنشائها، ويمكن لا يزال شرح الآثار الجانبيّة الخارجيّة.
  • إجراء UI واحد، أو عنصر إدخال واحد، أو حدّ jobs واحد قد يسمح بالاستمرار.
  • الحالة المشتركة القابلة للتغيير، والـ loops الأبويّة، وكود البدء، والحدود الـ native، وإشارات تلف الذاكرة تدفع القرار نحو الخروج.
  • الاستثناءات مثل StackOverflowException و AccessViolationException و OutOfMemoryException الخطير ليست أماكن جيّدة للبدء من افتراض «الاستمرار».
  • يمكن أحياناً تكوين WPF و Windows Forms للظهور بأنّها تستمرّ بعد استثناء غير معالج، لكنّ إمكانيّة الاستمرار وأمان الاستمرار ليسا الشيء نفسه.
  • بالنسبة للخدمات والمراقبات طويلة المدى، الانهيار وإعادة التشغيل غالباً ما يكون أكثر أماناً من البقاء نصف معطّل.

باختصار، المحور الحقيقيّ ليس «هل يمكننا catch؟» بل:

هل يمكننا استعادة الـ invariants التي كان من المفترض أن تظلّ صحيحة؟

2. ما الذي يعنيه هذا المقال بـ «استثناء غير متوقّع»

2.1 الإخفاقات المخطّط لها ليست الشيء نفسه

الاستثناء النادر ليس تلقائيّاً استثناءً غير متوقّع.

أمثلة قد لا تزال مخطّط لها تشمل:

  • اختار المستخدم ملفّاً لم يعد موجوداً
  • انتهت مهلة نقطة نهاية بعيدة مؤقّتاً
  • كان أحد الأسطر في CSV مستورد مشوّهاً
  • حدث OperationCanceledException لأنّ المستخدم ألغى
  • يجب أن يفشل خرق قاعدة عمل في تلك العمليّة فقط

حتى إن لم تحدث كثيراً، فهذه لا تزال شروطاً يمكنك التصميم لها.

الموضوع الرئيسيّ هنا مختلف:

  • انهار افتراض رئيسيّ في الكود وأدّى إلى NullReferenceException أو InvalidOperationException
  • كانت الحالة المشتركة قيد التحديث ومن غير الواضح إلى أيّ مدى وصل التحديث
  • خرج loop مراقبة أو رسائل أبويّ من خلال استثناء غير متوقّع
  • فشل شيء ما عند حدّ COM / P/Invoke / vendor SDK
  • يشير الاستثناء نفسه إلى أنّ العمليّة قد لا تكون موثوقة بعد الآن

بمعنى آخر:

بعد الاستثناء، هل لا يزال يمكن تصديق حالة التطبيق؟

2.2 الخيار ليس ثنائيّاً فعلاً

أحد أكبر مصادر الالتباس هو معاملة «الاستمرار» على أنّه شيء واحد فقط.

في الممارسة، هناك غالباً ثلاثة مستويات:

الخيار المعنى
إفشال العمليّة الحاليّة فقط والاستمرار تعيش العمليّة، لكنّ هذا الحفظ / الاستيراد / الطلب يفشل
عزل وإعادة تهيئة subsystem إعادة الاتّصال بخدمة واحدة، إعادة إنشاء شاشة واحدة، إعادة تشغيل worker واحد
إنهاء العمليّة لم يعد يمكن الوثوق بحدّ الضرر

قد يعني «استمرار التطبيق» أشياء مختلفة جدّاً.
الاستمرار بالتخلّص من وحدة فاشلة واحدة يختلف اختلافاً كبيراً عن التظاهر بأنّ شيئاً لم يحدث.

3. جدول القرار الأوّل

3.1 النظرة العامّة السريعة

غالباً ما يكفي هذا الجدول لتحديد الاتّجاه الأوّليّ.

الموقف الاتّجاه الأوّليّ لماذا
فشل إدخال واحد، أو إجراء UI واحد، أو job واحد ويمكن التخلّص من الحالة يميل إلى الاستمرار يمكن احتواء الفشل
يمكن تدمير الكائن أو الاتّصال المتأثّر وإعادة إنشائه يميل إلى إعادة تشغيل subsystem يمكن توطين الضرر
تمّ تحديث الحالة المشتركة جزئيّاً والحالة النهائيّة غير واضحة يميل إلى الخروج قد تكون الـ invariants قد كُسرت بالفعل
الآثار الجانبيّة الخارجيّة جزئيّة أو غامضة يميل إلى الخروج لم يعد يمكن شرح العالم الخارجيّ بوضوح
تسرّب استثناء غير متوقّع من loop مراقبة أو معالجة أبويّ يميل إلى الخروج السلوك الزومبي محتمل إن استمرّت العمليّة في العمل
فشل البدء أو تحميل الإعدادات أو wiring الـ DI أو تهيئة تبعيّة مطلوبة فشل بدء، ثمّ خروج البدء الجزئيّ عادةً ما يكون أسوأ
رائحة AccessViolationException أو StackOverflowException أو OutOfMemoryException خطير أو تلف native يميل إلى الخروج الفوريّ صحّة العمليّة بأكملها موضع شكّ
العمل المحفوف بالمخاطر معزول في عمليّة أخرى والأبوين سليمين إبقاء الأبوين على قيد الحياة، إعادة تشغيل الطفل تمّ بالفعل فصل مجال الفشل
flowchart TD
    A["Unexpected exception"] --> B{"Memory corruption / stack exhaustion / fatal resource failure smell?"}
    B -- "Yes" --> Z["Exit / FailFast / restart"]
    B -- "No" --> C{"Can the failed unit be discarded?"}
    C -- "No" --> Y["Lean toward exit"]
    C -- "Yes" --> D{"Can shared state be rolled back or reinitialized?"}
    D -- "No" --> X["Stop subsystem or exit"]
    D -- "Yes" --> E{"Can external side effects still be explained?"}
    E -- "No" --> X
    E -- "Yes" --> W["Fail only this operation and continue"]

3.2 ما يجب فحصه قبل نوع الاستثناء

نوع الاستثناء مهمّ، لكنّه يجب ألّا يكون العدسة الأولى والوحيدة.

الأسئلة الأكثر أهمّيّة هي:

ما يجب فحصه السؤال العمليّ
أين حدث حدث UI، job واحد، loop أبويّ، بدء، حدّ native
إلى أيّ مدى وصل هل تغيّرت حالة الذاكرة، أو حالة DB، أو حالة الملفّ، أو حالة الجهاز بالفعل؟
نطاق الانفجار المحتمل كائن واحد، شاشة واحدة، subsystem واحد، أو العمليّة بأكملها؟
مسار الإرجاع أو إعادة الإنشاء هل يمكن التخلّص من الشيء التالف وإعادة بنائه؟
الآثار الجانبيّة الخارجيّة هل تمّ بالفعل إرسال شيء، أو نقله، أو تحصيل ثمنه، أو الالتزام به؟
مسار إعادة التشغيل / الإشراف هل هناك قصّة إعادة تشغيل نظيفة إن اخترت الخروج؟

3.3 الاستثناءات التي تستحقّ شكّاً أقوى

لست بحاجة إلى تصنيف ضخم لاتّخاذ قرارات جيّدة، لكنّ بعض الحالات تستحقّ حذراً أقوى.

الاستثناء / الرائحة الاتّجاه الأوّليّ لماذا
StackOverflowException يميل إلى الخروج الفوريّ call stack نفسه في خطر
AccessViolationException يميل إلى الخروج الفوريّ تلف ذاكرة native أو وصول غير صالح يحدث
OutOfMemoryException يميل إلى الخروج منطق التعافي نفسه قد يحتاج إلى تخصيصات لم يعد يمكنه الاعتماد عليها
NullReferenceException / InvalidOperationException غير متوقّع يعتمد على السياق لكنّه يميل إلى الخروج افتراضاتك الخاصّة كُسرت، ربّما في منتصف التحديث
استثناء غير متوقّع يهرب من loop أبويّ يميل إلى الخروج إدارة الـ lifetime الأساسيّة قد تكون مكسورة بالفعل
إخفاقات قادمة من callbacks الـ COM / P/Invoke / vendor يميل بقوّة إلى الخروج الأدلّة من جانب managed قد تكون غير كاملة جدّاً للوثوق بالاستمرار

4. الحكم جزئيّاً بناءً على المكان الذي حدث فيه

4.1 أحداث UI

نقرات الأزرار، وانتقالات الشاشة، والبحث، وإجراءات اختيار الملفّات غالباً ما تترك مجالاً أكبر للاستمرار ممّا يعتقد الناس، ولكن فقط تحت شروط.

يصبح الاستمرار أكثر معقوليّة عندما:

  • حدث الفشل قبل تغيير حالة العمل
  • فقط حالة الحوار المؤقّتة تالفة ويمكن التخلّص منها
  • يمكن إعادة إنشاء الـ ViewModel أو الاتّصال
  • يمكن إخبار المستخدم بصدق بأنّ هذا الإجراء الواحد قد فشل

يصبح الاستمرار أضعف بكثير عندما:

  • تمّ تعديل كلّ من حالة UI وحالة domain جزئيّاً
  • تمّت ملامسة caches مشتركة، أو singletons، أو حالة قابلة للتغيير عبر الشاشات
  • لا يزال UI ما بعد الاستثناء يعرض عناصر تحكّم بدون حالة موثوقة خلفها

في تطبيقات UI، تكون غريزة تجنّب الانهيار قويّة.
لكنّ زرّ حفظ مكسور قد يكون أسوأ من خروج نظيف.

4.2 jobs بعنصر واحد أو حدود الطلبات

هذا أحد أكثر الأماكن صحّةً لاحتواء الفشل:

  • رسالة واحدة
  • ملفّ واحد
  • طلب HTTP واحد
  • job استيراد واحد
  • عنصر دفعة واحد

إن كانت هذه الوحدة صريحة، فقد تكون قادراً على إفشال هذه الوحدة فقط والاستمرار مع التالية.

لكنّ الاستمرار لا يزال يعتمد على أشياء مثل:

  • الوحدة الفاشلة مرئيّة بوضوح من الخارج
  • يمكن إرجاع التغييرات الجزئيّة أو التعويض عنها
  • إعادة تشغيل الوحدة نفسها آمن بما يكفي
  • يمكن تحويل العناصر الفاشلة إلى logs أو معالجة dead-letter

4.3 loops المقيمة، والمراقبات، ومعالجات الـ queues

هذا هو المكان الذي يصبح فيه الاستمرار الزائف خطيراً بشكل خاصّ.

أمثلة:

  • loops إعادة الاتّصال
  • loops المراقبة
  • loops استهلاك queues
  • loops الاستطلاع الدوريّ
  • workers تطبيقات tray المقيمة

أسوأ نمط فشل هو:

يموت الـ loop الأبويّ مرّة واحدة، تبقى العمليّة على قيد الحياة، ويتوقّف التطبيق بصمت عن القيام بعمله الحقيقيّ.

لهذا السبب يهمّ هذا التقسيم:

  • التقاط الإخفاقات المتوقّعة عند حدّ العنصر
  • إن هرب استثناء غير متوقّع من الـ loop الأبويّ، يميل نحو الخروج من العمليّة

خاصّةً في الخدمات والتطبيقات طويلة المدى، إعادة التشغيل النظيفة بواسطة supervisor غالباً ما تكون أكثر صحّةً من العيش في حالة نصف ميّتة.

4.4 البدء

«دعه يبدأ على أيّ حال ولنرَ ما يحدث» عادةً ما لا تكون سياسة بدء جيّدة.

أمثلة يجب أن تفشل في البدء وتخرج في الغالب:

  • تعذّر تحميل الإعدادات المطلوبة
  • فشل migration
  • مجلّد أو شهادة مطلوبة مفقودة
  • فشلت تهيئة البنية التحتيّة الأساسيّة
  • إعدادات التبعيّة مكسورة

البدء الجزئيّ غالباً ما يكون أكثر إرباكاً وأكثر خطورة من فشل بدء واضح.

4.5 الحدود الـ native: COM, P/Invoke, unsafe

هذه تستحقّ عدسة أكثر صرامة.

  • COM
  • P/Invoke
  • C++/CLI خارج الحدّ
  • vendor SDKs
  • callbacks من كود native
  • مسارات كود unsafe

في تلك الحالات، الاستثناء المُدار غالباً ما يكون مجرّد العَرَض السطحيّ.
الضرر الحقيقيّ قد يوجد بالفعل على الجانب الـ native، حيث لم يعد بإمكان الـ runtime رواية قصّة نظيفة.

5. الشروط التي تدعم الاستمرار

يصبح الاستمرار أكثر معقوليّة بكثير عندما تتوافق هذه الشروط:

الشرط المعنى
الوحدة الفاشلة صريحة عمليّة واحدة، شاشة واحدة، job واحد، اتّصال واحد
يمكن التخلّص من الحالة التالفة التخلّص وإعادة الإنشاء، أو معاملتها كغير ملتزَم بها
تظلّ الحالة المشتركة محميّة لا ينتشر الضرر عبر بقيّة التطبيق
الآثار الجانبيّة الخارجيّة قابلة للفهم تعرف ما إذا تمّ إرسال شيء، أو الالتزام به، أو يحتاج إلى إعادة محاولة
يمكن إخبار المستخدم بالحقيقة «فشلت هذه العمليّة» رسالة صالحة
يمكن ملاحظة الفشل لاحقاً logs، metrics، dumps، أو أدوات تشخيص أخرى موجودة

هذا النوع من الاستمرار ليس «التظاهر بأنّه لم يحدث أبداً».
بل هو:

  • إفشال هذه الوحدة بصدق
  • التخلّص من الكائن التالف أو إعادة بنائه
  • إعادة الاتّصال أو إعادة الإنشاء إن لزم الأمر
  • السماح للعمليّة التالية بالبدء من حالة معروفة جيّدة

6. الشروط التي تدعم الخروج

يصبح الخروج هو الاتّجاه الأكثر أماناً عندما:

  • لا تعرف ما الذي تغيّر جزئيّاً
  • قد تكون الحالة المشتركة القابلة للتغيير غير متّسقة بالفعل
  • قد تكون الـ locks، أو الـ queues، أو الـ threads، أو lifetimes الـ loops مكسورة
  • الآثار الجانبيّة الخارجيّة جزئيّة أو غامضة
  • فشل البدء أو البنية التحتيّة الأساسيّة
  • يشير الاستثناء إلى تلف الذاكرة أو الحدّ الـ native

في تلك المرحلة، تساعد جهود جعل الاستمرار يبدو أنيقاً عادةً أقلّ من جهود جعل الفشل وإعادة التشغيل أسهل.

هذا هو المكان الذي تبدأ فيه خيارات التصميم مثل هذه في أن تصبح أكثر أهمّيّة:

  • إعادة التشغيل التلقائيّ
  • استرداد الجلسة
  • الحفظ التلقائيّ
  • queues قابلة لإعادة التشغيل
  • إعادة التنفيذ idempotent
  • crash dumps وتسجيل الفشل

7. الحالات النموذجيّة والاتّجاه العمليّ

النمط الاتّجاه المقترح لماذا
اختار المستخدم ملفّاً مفقوداً إفشال تلك العمليّة والاستمرار الضرر موضعيّ
سطر CSV واحد مشوّه إفشال سطر واحد أو ملفّ واحد والاستمرار وحدة الفشل قابلة للاحتواء
NullReferenceException غير متوقّع أثناء الحفظ إعادة بناء الشاشة أو الميل إلى الخروج قد تكون الحالة قد تغيّرت جزئيّاً بالفعل
عنصر queue واحد انتهك قاعدة عمل إفشال هذا العنصر والاستمرار معالجة بأسلوب dead-letter ممكنة
تسرّب استثناء غير متوقّع من loop أبويّ لمستهلك queue الخروج من العمليّة قد تكون إدارة الـ lifetime مكسورة بالفعل
إعدادات مطلوبة مفقودة عند البدء فشل البدء والخروج البدء الجزئيّ أسوأ
AccessViolationException بالقرب من callback vendor يميل إلى الخروج الفوريّ سلامة الذاكرة الـ native الآن موضع شكّ
فشل رفع telemetry اختياريّ تعطيل ذلك المسار فقط والاستمرار مجال الفشل منفصل عن العمل الأساسيّ

8. الأنماط السيّئة الشائعة

8.1 catch لكلّ شيء، تسجيله، والاستمرار

هذا يخفي السبب ويمدّد حالة قد تكون مكسورة.
هو أحد أكثر أشكال الأمان الزائف تكلفةً.

8.2 معاملة hook الاستثناء غير المعالج النهائيّ كنقطة استرداد

Hooks مثل:

  • AppDomain.UnhandledException
  • Application.ThreadException
  • DispatcherUnhandledException

مفيدة كـ نقاط تسجيل أخيرة.
هي عادةً ليست نقاط استرداد سحريّة جيّدة.

8.3 إعادة المحاولة بشكل أعمى عندما تكون الآثار الجانبيّة الخارجيّة قد حدثت بالفعل

إن كان الإجراء قد أرسل بالفعل أمراً، أو فرض رسماً على شيء، أو نقل ملفّاً، أو التزم بسجلّ، فإنّ إعادة المحاولة العمياء يمكن أن تحوّل فشلاً واحداً إلى حادث إجراء مكرّر.

8.4 ترك UI على قيد الحياة بعد موت الـ worker الحقيقيّ

التطبيق الذي لا يزال يبدو مفتوحاً ولكنّه لم يعد يقوم بعمله الحقيقيّ عادةً ما يكون أسوأ من التطبيق الذي يخرج بوضوح.

8.5 قول «يجب ألّا ننهار» دون التصميم للانهيار

إن كان يجب على التطبيق فعلاً أن ينجو من الفشل بأناقة، فيجب أن يوجد عمل التصميم أوّلاً:

  • استراتيجيّة إعادة التشغيل
  • استرداد الجلسة
  • التقدّم المحفوظ
  • أمان إعادة التشغيل
  • فصل مجال الفشل

وإلّا فإنّ «لا تنهار» مجرّد أمنية.

9. إرشادات التنفيذ

9.1 catch عند الحدود، وليس في كلّ مكان

الـ catching بحدود ذات معنى أكثر صحّةً بكثير من الـ catching في كلّ طبقة عميقة:

  • حدود إجراءات UI
  • حدود الطلبات
  • حدود jobs
  • حدود الاتّصالات
  • حدود العمليّات

9.2 افصل الإخفاقات المتوقّعة عن الإخفاقات غير المتوقّعة

  • متوقّعة: validation، not-found، timeout، cancellation، رفض قاعدة العمل
  • غير متوقّعة: انهيار افتراض، هروب loop أبويّ، فشل حدّ native، رائحة تلف

بمجرّد أن يتدفّق كلّ شيء إلى catch (Exception) واحد واسع، تنخفض جودة القرار بسرعة.

9.3 اجعل الحالة المشتركة القابلة للتغيير أصغر

كلّما كانت الحالة المشتركة القابلة للكتابة أكبر، أصبح الاستمرار الآمن أصعب.
كلّما تمكّنت من احتواء الحالة في شاشة واحدة، أو جلسة واحدة، أو worker واحد، تمكّنت من احتواء الفشل أيضاً.

9.4 اعزل العمل الخطير في عمليّة أخرى

COM و ActiveX و vendor SDKs و كود unsafe والتحكّم بالأجهزة الخارجيّة والأسطح المحفوفة بالمخاطر المماثلة تصبح أسهل بكثير في التفكير فيها عندما يُنقل نطاق الانفجار إلى عمليّة أخرى.

9.5 عامل hooks الاستثناء غير المعالج كـ hooks تسجيل

ركّز عليها على:

  • معلومات الاستثناء
  • سياق العمليّة
  • logs السابقة المهمّة
  • بيانات الإصدار والبيئة
  • مسارات التقاط dump

هذا عادةً ما يساعد أكثر من محاولة التظاهر بأنّ التطبيق صحّيّ مرّة أخرى الآن.

9.6 لا تثق كثيراً بـ hooks الاستثناء غير المعالج لـ WPF / WinForms

يمكن لـ WPF الاستمرار بعد DispatcherUnhandledException إن تمّ تعيين Handled = true.
يمكن لـ Windows Forms أيضاً تغيير سلوك المعالجة لـ thread الـ UI.

لكنّ السؤال الحقيقيّ ليس أبداً «هل يمكنه الاستمرار؟»
بل هو دائماً:

هل الحالة ما بعد الاستثناء موثوقة بما يكفي للاستمرار؟

10. الختام

بعد استثناء غير متوقّع، السؤال الحقيقيّ ليس:

«هل يمكننا catch؟»

السؤال الحقيقيّ هو:

هل لا يزال يمكننا الوثوق بحالة التطبيق بعد ذلك؟

ترتيب تفكير مفيد هو:

  1. هل يمكن التخلّص من الوحدة الفاشلة؟
  2. هل يمكن إصلاح الحالة المشتركة أو إعادة بنائها؟
  3. هل لا يزال يمكن شرح الآثار الجانبيّة الخارجيّة؟
  4. هل لا يزال يمكن الوثوق بالذاكرة و الـ threads والحدود الـ native؟

إن كانت الإجابة نعم عبر تلك النقاط، فقد يكون الاستمرار آمناً.
إن كانت الإجابة غير واضحة، فإنّ الخروج غالباً ما يكون المسار الأكثر صدقاً وأماناً.

معالجة الاستثناءات ليست في الأساس تقنيّة لـ «عدم الانهيار أبداً».
هي انضباط تصميم لاحتواء الضرر، والفشل بصدق، والتعافي بنظافة.

11. مراجع

مقالات ذات صلة

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

أين يتصل هذا الموضوع

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

العودة إلى المدونة