أين يُوضَع catch والسجلّ في معالجة الاستثناءات

· آخر تحديث: · · معالجة الاستثناءات, السجلّات, معالجة الأخطاء, التصميم, C# / .NET

سجل التعديلات (2 تحديثات، آخر تحديث 2 Sep، 2026)

سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.

أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621499)

هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.

小村 豪 (2026). أين يُوضَع catch والسجلّ في معالجة الاستثناءات. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621499 https://comcomponent.com/ar/blog/2026/04/15/000-exception-catching-logging-error-handling/

DOI (أحدث نسخة)
10.5281/zenodo.21621499
DOI (هذه النسخة)
10.5281/zenodo.22240917

الملاحظات التي تتكرّر في مراجعة كود معالجة الاستثناءات تتجمّع في الغالب في ثلاثة.

  • أعمق دالّة مشتركة تستعمل catch (Exception)، فلا يستطيع المستدعي التمييز بين «لم تكن هناك بيانات» و«انكسر شيء في المنتصف»
  • عطل واحد، ومع ذلك يصطفّ الـ stack trace نفسه أربع مرّات في Repository وService وController ومعالج الاستثناء غير المعالَج
  • المستخدم ألغى فقط، ومع ذلك يُكتب سجلّ Error فتُدفن الأعطال الخطرة حقّاً في ذلك الضجيج

ليس السبب أنّ كتابة try / catch رديئة. السبب أنّ أين يُستقبَل، ومن يكتب السجلّ، وأين يُحسَم شكل الإخفاق لم يُقسَم كأدوار. إن لم تُقسَم الأدوار، يضيف كلّ مطوّر في كلّ طبقة catch وسجلاً «احتياطاً»، فينتج كوداً لا يُرى سببه.

هذه المقالة موجَّهة إلى من يكتب تطبيقات أعمال أو Web API بـ C# / .NET، وإلى من يراجع ذلك التصميم، وترتيب حدّ التقاط الاستثناء، وموضع السجلّ الرئيسيّ، ومسؤوليّة حكم التعافي. إن حُسم مسبقاً ماذا تفعل كلّ طبقة في تسلسل الاستدعاء، قلّ تذبذب الحكم في المراجعة وفي تحقيق الأعطال.

مصطلحات هذه المقالة

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

المصطلح المعنى في هذه المقالة
وحدة الإخفاق وحدة معالجة متماسكة في العمل تبيّن «ماذا أخفق مرّة واحدة». نقرة شاشة واحدة، أو طلب HTTP واحد، أو وظيفة واحدة، أو رسالة واحدة، أو صفّ CSV واحد. تظهر هذه الوحدة في السجلّ وفي الاستجابة
السجلّ الرئيسيّ سجلّ Error أو Critical يُكتَب مرّة واحدة لإخفاق واحد. يصحبه وحدة الإخفاق وسياق تشغيليّ (requestId، userId، معرّف الهدف، وغيرها). ما سواه سجلات مساعدة تُعالَج بـ Debug / Information / Warning
التحويل إلى نتيجة التوقّف عن رمي الاستثناء إلى الأعلى، وتحويله إلى قيمة عودة مثل نوع Result أو DTO يمثّل الإخفاق. عمليّة تجعل الإخفاق المتوقّع قابلاً للمعالجة بفرع لدى المستدعي

جدول المحتويات

  1. الخلاصة أوّلاً
  2. catch والسجلّ ومعالجة الأخطاء أمور مختلفة
    • 2.1. فعل catch
    • 2.2. كتابة السجلّ
    • 2.3. معالجة الأخطاء
    • 2.4. ترجمة الاستثناء
  3. جدول القرار الأوّل الذي ننظر إليه
  4. ماذا نفعل في كلّ مستوى من تسلسل الاستدعاء
    • 4.1. أعمق helper / utility / private method
    • 4.2. حدود I/O الخارجيّ: Repository / Gateway / SDK wrapper
    • 4.3. Application Service / UseCase
    • 4.4. حدود UI / HTTP / Job / Message
    • 4.5. المعالج النهائيّ للاستثناء غير المعالَج
    • 4.6. النظر إلى تسلسل استدعاء واحد من البداية إلى النهاية
  5. افصل الإخفاقات المتوقّعة عن الاستثناءات غير المتوقّعة
  6. أين تسجّل، وكم مرّة
  7. أنماط مضادّة شائعة
  8. قائمة فحص للمراجعة
  9. دليل سريع تقريبيّ
  10. الخلاصة
  11. المراجع
  12. مقالات ذات صلة

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 25، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

1. الخلاصة أوّلاً

  • المبدأ عدم استعمال catch واسع في الطبقات العميقة. ضع catch بالقرب من الحدّ الذي يمكنك فيه تعريف وحدة الإخفاق.
  • بالنسبة إلى السجلّات، يجب أن يكون الافتراض سجلاً رئيسيّاً واحداً لإخفاق واحد. إذا استمرّت كلّ طبقة في كتابة Error للاستثناء نفسه، يخسر القارئ.
  • مسؤوليّة أعمق طبقة هي التنظيف، والـ rollback المحلّيّ، وترجمة الاستثناء، وretry محدود فقط عند الاقتضاء. إذا أعادت الرمي، فهي عادةً لا تكتب السجلّ الرئيسيّ هناك.
  • حدود المعالجة مثل إجراء شاشة واحد، أو طلب HTTP واحد، أو تشغيل وظيفة واحد، أو وحدة معالجة رسالة واحدة، عادةً ما تكون أكثر الأماكن طبيعيّةً للسجلّ الرئيسيّ.
  • الإخفاقات المتوقّعة يجب أن تُحوَّل إلى نتائج عند وحدة حالة الاستخدام. لا يلزم أن تستمرّ في رمي كلّ شيء إلى الأعلى كاستثناء إلى الأبد.
  • AppDomain.UnhandledException، و DispatcherUnhandledException في WPF، و ThreadException في WinForms، ومعالجات استثناءات ASP.NET Core، والمعالجة النهائيّة لاستثناء المضيف، هي آخر مكان للتسجيل أكثر ممّا هي نقطة تعافٍ.
  • OperationCanceledException المتعلّق بإلغاء المستخدم أو إيقاف التشغيل عادةً لا يُعامل كـ Error.
  • عند الشكّ، تحقّق بهذا الترتيب.
    1. هل يمكن لهذا المكان فعلاً اتّخاذ القرار؟
    2. هل وحدة الإخفاق مرئيّة هنا؟
    3. هل يمكن إعادة الحالة أو إعادة بنائها هنا؟
    4. إذا سجّلت هنا، فهل سيُسجَّل الاستثناء نفسه أيضاً في الأعلى؟

الفكرة الجوهريّة بسيطة: لا تلتقط حيث تستطيع الالتقاط فقط؛ بل التقط حيث يمكنك اتّخاذ قرار مسؤول.

2. catch والسجلّ ومعالجة الأخطاء أمور مختلفة

2.1. فعل catch

catch يعني تلقّي استثناء مرّة واحدة وتغيير تدفّق التحكّم بسببه. لكنّ ذلك بحدّ ذاته ليس تعافياً.

على سبيل المثال، حتّى لو التقطت طريقة أدنى استثناءً، فقد لا تعرف بعد:

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

إذا لم تستطع الإجابة عن هذه الأسئلة، فإنّ هذا المكان غالباً ليس مكاناً جيّداً لـ catch.

2.2. كتابة السجلّ

التسجيل ليس فقط تدوين أنّ استثناءً قد حدث. بل هو تدوين أيّ جزء من العمل أخفق حتّى تستطيع تتبّعه لاحقاً.

لذلك تحتوي نقاط التسجيل الجيّدة عادةً على واحد أو أكثر ممّا يلي:

  • requestId / traceId
  • userId
  • orderId / fileId / batchId
  • أيّ عنصر إدخال أخفق
  • أيّ إجراء UI كان
  • أيّ قائمة انتظار أو أيّ رسالة كانت

المساعدات العميقة والوظائف المشتركة غالباً تعرف التفاصيل التقنيّة لكن لا تملك هذا السياق التشغيليّ. لذلك فإنّ المكان الذي يعرف التفاصيل التقنيّة و المكان الذي يعرف السياق التشغيليّ غالباً ما يكونان مكانين مختلفين.

2.3. معالجة الأخطاء

هنا، تعني “معالجة الأخطاء” أشياء مثل:

  • عرض رسالة خطأ على الشاشة
  • إعادة 4xx / 5xx في HTTP
  • إخفاق عنصر واحد فقط والمتابعة إلى التالي
  • إعادة تهيئة نظام فرعيّ
  • إيقاف العمليّة وترك سياسة إعادة التشغيل تتولّى
  • تحرير الموارد والخروج بأمان

بعبارة أخرى، تعني تحديد الشكل المرئيّ للإخفاق بالنسبة إلى المستدعي أو المستخدم.

2.4. ترجمة الاستثناء

في الأنظمة الحقيقيّة، توجد خطوة مهمّة أخرى بين catch و”معالجته”. هذه الخطوة هي الترجمة.

على سبيل المثال:

  • HttpRequestException
  • IOException
  • JsonException
  • الاستثناءات الخاصّة ببرنامج تشغيل قاعدة البيانات
  • الاستثناءات الخاصّة بـ vendor SDK

إذا تسرّبت تلك مباشرةً إلى UI أو Controller، تبدأ الطبقات العليا بتعلّم تفاصيل تنفيذ الطبقات الأدنى.

لذلك عند الحدّ، من الأفضل غالباً ترجمتها إلى إخفاقات ذات معنى في تلك الطبقة، مثل:

  • “تعذّر الاتّصال بخدمة الدفع”
  • “كان تنسيق CSV غير صالح”
  • “تعذّر الكتابة إلى وجهة الحفظ”
  • “كانت استجابة الجهاز غير صالحة”

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

3. جدول القرار الأوّل الذي ننظر إليه

تظهر في هذه المقالة ثلاثة جداول متشابهة الشكل. أدوارها مختلفة، فنكتب الفصل أوّلاً.

الجدول متى يُنظَر إليه ماذا فيه
جدول القرار في الفصل 3 عند التصميم. حين تُقسَم مسؤوليّة كلّ طبقة السياسة الأساسيّة لكلّ موضع، وهل يُكتب السجلّ الرئيسيّ، والمسؤوليّة الرئيسة
جدول نقاط السجلّ في الفصل 6 عند التنفيذ. حين تتوقّف اليد عن كتابة سطر سجلّ لكلّ نوع إخفاق: أين يُسجَّل وبأيّ مستوى
دليل الاستعمال التقريبيّ في الفصل 9 عند المراجعة. للتأكيد الأخير طيّ الفصلين 3 و6 في ثلاثة أعمدة: catch / السجلّ / معالجة الأخطاء

يصبح البقاء منظّماً أسهل بكثير إذا قرّرت أوّلاً السياسة العامّة بجدول كهذا.

المكان السياسة الأساسيّة السجلّ الرئيسيّ المسؤوليّة الرئيسة
helper / utility / private method كقاعدة، لا تستخدم catch على نطاق واسع لا شيء التنظيف في finally، rollback محلّيّ، إضافة الحدّ الأدنى من السياق
Repository / Gateway / SDK wrapper التقط فقط استثناءات محدّدة عادةً لا ترجمة الاستثناءات، retry محدود، التخلّص من الاتّصالات أو المقابض المكسورة
Application Service / UseCase حوّل الإخفاقات المتوقّعة إلى نتائج إذا تمّ ابتلاعه، فقط حسب الحاجة هنا تعريف وحدة الإخفاق، السماح بالإخفاق الجزئيّ، اتّخاذ قرارات على مستوى حالة الاستخدام
حدّ UI / Controller / API / Job / Message المستقبل الرئيسيّ للاستثناءات غير المتوقّعة غالباً هنا استجابة المستخدم، استجابة HTTP، قرار المتابعة-إلى-التالي أو الإلغاء
معالج الاستثناء غير المعالَج / الحدّ النهائيّ للمضيف خطّ الدفاع الأخير للحالات المفقودة Critical التسجيل النهائيّ، flush، dump، مسار الخروج / إعادة التشغيل

بصريّاً، يبدو الأمر عادةً كالتالي:

لانعملانعملانعمحدث استثناءهل يستطيع هذا المكان تقرير retry / التحويل إلى نتيجة / الاستمرار أو التوقّف؟كقاعدة لا تلتقط هنا؛ أرسله إلى الأعلىهل هذا حدّ طبقة؟تنظيف محلّيّ فقطترجم إن لزم إلى استثناء ذي معنىهل يمكن هنا تمييز وحدة الإخفاق والسياق التشغيليّ؟لا تكتب السجلّ الرئيسيّ هنا؛ أرسل إلى الأعلىاكتب سجلاً رئيسيّاً واحداً وقرّر الاستجابةإن لزم: خروج / إعادة تهيئة / متابعة العنصر التالي

توجد فكرتان أساسيّتان في هذا الرسم.

  1. السبب الأوّل لـ catch هو التعافي أو التنظيف، وليس التسجيل
  2. السبب الأوّل للتسجيل هو اكتمال السياق التشغيليّ، وليس مجرّد ملاحظتك لاستثناء

4. ماذا نفعل في كلّ مستوى من تسلسل الاستدعاء

4.1. أعمق helper / utility / private method

في هذا المستوى، القاعدة الافتراضيّة هي عدم الالتقاط على نطاق واسع.

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

  • أيّ إجراء شاشة كان ينتمي إليه
  • أيّ طلب كان ينتمي إليه
  • هل يجب أن تخفق هذه العمليّة فقط
  • هل يجب إيقاف الشاشة كلّها

ما يُسمح لهذه الطبقة بفعله هو في الغالب:

  • تحرير الموارد في finally
  • إجراء rollback للحالة المحلّيّة التي كسرتها جزئيّاً
  • إضافة سياق قليل فقط إلى رسالة الاستثناء
  • استبدالها بنوع استثناء أكثر ملاءمةً
  • التخلّص من الكائنات التي لم تعد قابلةً لإعادة الاستخدام

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

الأمور التي يُفضَّل تجنّبها هنا هي:

  • catch (Exception) وإرجاع null / false / مصفوفة فارغة
  • عرض MessageBox هنا
  • كتابة سجلّ Error هنا ثمّ إعادة الرمي
  • “المتابعة بطريقة ما” عندما لا يمكن استعادة الحالة فعليّاً

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

4.2. حدود I/O الخارجيّ: Repository / Gateway / SDK wrapper

هذه طبقة تكون فيها أسباب catch أوضح بكثير.

لماذا؟ لأنّ التفاصيل الخاصّة بالتنفيذ من الطبقات الأدنى تظهر هنا:

  • استثناءات برنامج تشغيل قاعدة البيانات
  • استثناءات اتّصال HTTP
  • استثناءات I/O للملفّات
  • الاستثناءات الخاصّة بـ COM / P/Invoke / vendor SDK
  • استثناءات المحلّل أو المسلسل

المسؤوليّات النموذجيّة في هذه الطبقة هي هذه الأربعة:

  1. التقط استثناءات محدّدة التقط استثناءات محدّدة ذات معنى بدلاً من Exception واسع.

  2. ترجمها إلى إخفاقات ذات معنى حتّى لا تحتاج الطبقات العليا إلى معرفة تفاصيل تنفيذ الطبقات الأدنى مباشرةً.

  3. إذا كان retry المحلّيّ مناسباً، فافعله هنا لكن فقط ضمن شروط صارمة نوعاً ما:
    • من المعروف أنّ الإخفاق عابر
    • العمليّة idempotent
    • عدد الـ retry وسياسة التأخير مُعرَّفة
    • السلوك النهائيّ بعد الإخفاق واضح ينتمي retry إلى هنا فقط عند استيفاء هذه الشروط.
  4. تخلّص من الاتّصالات أو المقابض المكسورة في كثير من الحالات، “إعادة إنشاء الاتّصال” أكثر أماناً من “الاستمرار في استخدام نفس الكائن في المرّة القادمة”.

بالنسبة إلى التسجيل في هذه الطبقة، تساعد السياسة التالية في تجنّب الالتباس:

  • إذا أعدت الرمي إلى الأعلى، فعادةً لا تكتب السجلّ الرئيسيّ هنا
  • إذا ابتلعت هذه الطبقة الاستثناء وحوّلته إلى نتيجة، فإنّ هذه الطبقة تمتلك السجلّ أو المقياس اللازم
  • أثناء retry، احتفظ بالمحاولات الفرديّة في Debug / Information / Warning، و سجّل الإخفاق النهائيّ فقط بشكل أقوى

هذه الطبقة عادةً هي حيث تحدث الترجمة، وليس حيث يُتّخذ القرار النهائيّ المرئيّ.

4.3. Application Service / UseCase

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

تشمل الأمثلة:

  • عمليّة حفظ
  • إنهاء طلب
  • استيراد CSV
  • معالجة عنصر دفعة واحد
  • تطبيق رسالة واحدة

هذه وحدات متماسكة على مستوى حالة الاستخدام.

تستطيع هذه الطبقة اتّخاذ قرارات مثل:

  • خطأ التحقّق من الصحّة يجب أن يخفق هذه المرّة فقط
  • NotFound يجب أن يصبح ما يعادل 404
  • انتهاك قاعدة عمل يجب إرجاعه لتصحيح المستخدم
  • صفّ CSV واحد غير صالح يجب تسجيله كـ Warning ويجب أن تستمرّ المعالجة
  • إخفاق مؤقّت لخدمة خارجيّة يجب أن يُخفق العمليّة بأكملها
  • العمل الجزئيّ يجب التخلّص منه وإعادة المحاولة من البداية

بعبارة أخرى، هذا هو المكان الذي يمكن فيه غالباً تعريف وحدة الإخفاق.

الاستخدامات الجيّدة النموذجيّة لهذه الطبقة هي:

  • تحويل الإخفاقات المتوقّعة إلى كائنات Result أو DTOs إخفاق
  • تجميع الإخفاقات الجزئيّة
  • تحديد عدد الإخفاقات التي قد يُسمح بها قبل المتابعة
  • التحويل إلى رموز خطأ أو مفاتيح رسائل المستخدم

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

4.4. حدود UI / HTTP / Job / Message

هنا يكون نقطة السجلّ الرئيسيّة غالباً في التطبيقات الحقيقيّة.

تشمل الأمثلة:

  • نقرة واحدة على زرّ “حفظ” في WinForms / WPF
  • طلب HTTP واحد في ASP.NET Core
  • رسالة worker واحدة
  • عنصر إدخال واحد في دفعة
  • تشغيل وظيفة مجدولة واحد

هذا الموقع يعرف:

  • ما العمليّة التي كانت
  • من بدأها
  • أيّ رقم عنصر كان
  • أيّ طلب / دفعة / رسالة كانت
  • ما الذي يجب إرجاعه للمستخدم أو المستدعي إذا أخفقت

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

  • التقاط الاستثناءات غير المتوقّعة على نطاق واسع
  • كتابة سجلّ رئيسيّ واحد مع السياق
  • التحويل إلى مربّع حوار خطأ، أو HTTP 500، أو Problem Details، أو إخفاق وظيفة، أو سلوك المتابعة-إلى-التالي

النقطة المهمّة ليست مجرّد أنّه يلتقط على نطاق واسع، بل أنّ ما يجب إرجاعه بعد الالتقاط مُعرَّف بالفعل هنا.

بالنسبة إلى معالجة الدفعات أو قوائم الانتظار، يساعد غالباً فصل مستويين:

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

“إخفاق عنصر واحد ومتابعة” و “بقاء الحلقة الأمّ على قيد الحياة من كلّ استثناء غير متوقّع بصمت” تصميمان مختلفان جدّاً.

4.5. المعالج النهائيّ للاستثناء غير المعالَج

هذا خطّ الدفاع الأخير. ليس نقطة تعافٍ سحريّة.

الأمثلة النموذجيّة هي:

  • AppDomain.UnhandledException
  • WPF Application.DispatcherUnhandledException
  • WinForms Application.ThreadException
  • exception middleware أو معالجات ASP.NET Core
  • المعالجة النهائيّة للاستثناء حول Generic Host / worker / BackgroundService

مسؤوليّاته الرئيسيّة هي أمور مثل:

  • التسجيل النهائيّ
  • flushing
  • ترتيب جمع dump
  • تخزين معلومات الجلسة أو السياق الأخير
  • تعيين رموز الخروج ومسارات إعادة التشغيل

من الأفضل أيضاً عدم توقّع الكثير منه:

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

هناك أيضاً نقاط عمليّة خاصّة بـ .NET تستحقّ التذكّر:

  • AppDomain.UnhandledException للـ إخطار وتسجيل استثناء غير معالَج. وضع منطق تعافٍ كبير بعد تلك النقطة مخاطرة.
  • في WPF DispatcherUnhandledException، يوجد مسار حيث يُبقي Handled = true التطبيقَ حيّاً، لكن السؤال الأوّل هو ما إذا كان التعافي آمناً فعلاً.
  • WinForms ThreadException يمكن أيضاً أن يترك التطبيق في حالة غير معروفة حتّى بعد المعالجة.
  • exception middleware في ASP.NET Core يحتاج إلى أن يُوضع في وقت مبكّر بما يكفي في الـ pipeline لتلقّي الاستثناءات أسفل البثّ.
  • منذ .NET 6، الاستثناء غير المعالَج في BackgroundService يُسجَّل وافتراضيّاً يميل إلى إيقاف المضيف. في كثير من الحالات، الإيقاف وترك سياسة إعادة التشغيل تتولّى أكثر أماناً من ابتلاع كلّ شيء على نطاق واسع في الحلقة الأمّ.

تطبيقات سطح المكتب على وجه الخصوص غالباً ما يكون لها مسار يبدو “يلتقط ويتابع” بعد استثناء غير معالَج. لكن القدرة على المتابعة و الأمان في المتابعة ليسا الشيء نفسه.

4.6. النظر إلى تسلسل استدعاء واحد من البداية إلى النهاية

على سبيل المثال، تخيّل تدفّقاً كهذا:

حدّ UI / Controller / JobApplication Service / UseCaseDomain / منطق العملRepository / Gateway / SDK wrapperDB / HTTP / File / Vendor SDK

في تلك الحالة، تنفصل الأدوار عادةً تقريباً كالتالي.

زرّ الحفظ → SaveOrderUseCase → PaymentGateway → HTTP

  • PaymentGateway
    • يلتقط إخفاقات الاتّصال وتنسيقات الاستجابة غير الصالحة
    • يترجمها إلى شيء مثل “إخفاق اتّصال خدمة الدفع” أو “استجابة خدمة دفع غير صالحة”
    • يُجري retry هنا فقط عندما تبرّر الشروطُ ذلك
    • إذا أعاد الرمي، فهو عادةً لا يكتب السجلّ الرئيسيّ
  • SaveOrderUseCase
    • يحوّل الإخفاقات المتوقّعة مثل رفض الدفع إلى نتائج
    • يعامل الإخفاق كـ “إنهاء هذا الطلب فقط أخفق”
    • يشكّل الإخفاق حتّى تستطيع طبقات UI أو API إرجاعه بنظافة
  • معالج زرّ UI / Controller
    • يلتقط الاستثناءات غير المتوقّعة على نطاق واسع
    • يكتب السجلّ الرئيسيّ مع orderId، و userId، و requestId
    • يحوّل الإخفاق إلى مربّع حوار، أو 500، أو استجابة 503
  • معالج الاستثناء غير المعالَج
    • يسجّل فقط ما تسرّب إلى هذا الحدّ
    • يُجري جمع dump أو flush نهائيّاً
    • يعطي الأولويّة لمسار الخروج بدلاً من التعافي

مع هذا التقسيم، تظلّ التفاصيل التقنيّة مغلقةً في الأسفل، ويُلصق السياق التشغيليّ في الأعلى، وتُتّخذ القرارات عند الحدود.

ما تكتبه كلّ طبقة فعلاً من سجلات

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

الطبقة السجلّ المكتوب المستوى مثال الرسالة
PaymentGateway كلّ محاولة retry Warning يُعاد الاتصال بخدمة الدفع. attempt={Attempt}/{MaxAttempts}, orderId={OrderId}
PaymentGateway عند الترجمة وإعادة الرمي لا يُكتب ─ (السجلّ الرئيسيّ دور الحدّ)
SaveOrderUseCase عند تحويل إخفاق متوقّع إلى نتيجة Information رُفض دفع الطلب. orderId={OrderId}, reason={DeclineReason}
SaveOrderUseCase استثناء غير متوقّع لا يُكتب ─ (يُرسَل كما هو إلى الحدّ)
معالج زرّ UI / Controller السجلّ الرئيسيّ لاستثناء غير متوقّع Error فشل تأكيد الطلب. orderId={OrderId}, userId={UserId} + كائن الاستثناء
معالج الاستثناء غير المعالَج السجلّ النهائيّ Critical يُنهى العملية بسبب استثناء غير معالَج + كائن الاستثناء

النقطة أنّ Error يظهر سطراً واحداً فقط. محاولات retry نزلت إلى Warning، والإخفاق المتوقّع إلى Information، فالبحث بـ Error يصيب هذا العطل كحالة واحدة.

// السجلّ الرئيسيّ. مرِّر كائن الاستثناء وسيطاً أوّل، وألحق سياق وحدة الإخفاق بأسماء
_logger.LogError(ex, "فشل تأكيد الطلب. orderId={OrderId}, userId={UserId}",
    orderId, userId);

إن نسيت تمرير كائن الاستثناء وسيطاً أوّل، لا يُسجَّل الـ stack trace. تمرير سلسلة فقط مثل _logger.LogError(ex.Message) يمنع تتبّع السبب لاحقاً.

5. افصل الإخفاقات المتوقّعة عن الاستثناءات غير المتوقّعة

أهمّ شيء في هذا الموضوع كلّه هو عدم معاملة كلّ شيء كنفس النوع من “الاستثناء”.

يساعد فصلها تقريباً كالتالي:

نوع الإخفاق المكان الأوّل لمعالجته المعالجة النموذجيّة
مشكلة تحقّق من الصحّة UseCase / حدّ الطلب إعادتها كخطأ إدخال
NotFound / Conflict UseCase / Controller 404 / 409 أو رسالة شاشة
إلغاء المستخدم / إيقاف التشغيل حدّ العمليّة إلغاء؛ عادةً ليس Error
صفّ CSV واحد غير صالح حدّ العنصر الواحد تسجيله كـ Warning ومتابعة
timeout عابر ينتهي مع ذلك بإخفاق من حدّ I/O إلى حدّ الطلب الإخفاق بعد retry
NullReferenceException، افتراضات مكسورة حدّ الطلب / الوظيفة السجلّ الرئيسيّ واستجابة الإخفاق
AccessViolationException، OutOfMemoryException شديد، أو رائحة فساد على حدّ native الحدّ النهائيّ معاملته كـ Critical والانتقال نحو إيقاف التشغيل

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

فصل هذين الاثنين فقط يقلّل من مشاكل مثل:

  • تسجيل NotFound كـ Error في كلّ مرّة
  • معاملة إلغاء المستخدم كانقطاع
  • السماح للإخفاقات الخطيرة فعلاً للافتراضات المكسورة بالاستمرار كما لو كانت مجرّد “أخفق هذا الطلب فقط”

6. أين تسجّل، وكم مرّة

عند تصميم السجلّات، غالباً ما يكون قرار من يملك السجلّ الرئيسيّ أهمّ من قرار المكان الدقيق لـ catch.

القواعد الأساسيّة هي:

  1. سجلّ Error / Critical رئيسيّ واحد لإخفاق واحد
  2. الطبقات الأدنى تضيف الترجمة والسياق فقط عند الحاجة
  3. الحدود العليا تكتب السجلّ الرئيسيّ مع وحدة الإخفاق والسياق التشغيليّ
  4. فقط الطبقة التي تبتلع الإخفاق تتحمّل المسؤوليّة الكاملة عن تسجيل ذلك الإخفاق المبتلع
  5. الإخفاقات المتوقّعة لا ينبغي أن تصبح دائماً Error
  6. OperationCanceledException يجب فصله عن سجلّات الإخفاق العاديّة

جدول تقريبيّ لنقاط التسجيل يبدو كالتالي. جدول الفصل 3 كان «أيّ مسؤوليّة تُوضَع في أيّ طبقة»، أمّا هذا فجدول لكلّ نوع إخفاق: أين تُترك أيّ مستوى من السجلّ. إن توقّفت أثناء التنفيذ عند «هل أكتب سجلاً في هذا catch؟»، فانظر هنا.

الوضع المكان الرئيسيّ للتسجيل المستوى النموذجيّ ملاحظة
خطأ تحقّق من الصحّة حدّ الطلب / حالة الاستخدام Information أو لا سجلّ ليس انقطاعاً، بل إخفاق عقد
إلغاء المستخدم / إيقاف التشغيل حدّ العمليّة Debug / Information عادةً ليس Error
إخفاق عابر أثناء retry الطبقة التي تمتلك retry Debug / Warning لا تُحدث ضوضاء قبل الإخفاق النهائيّ
إخفاق بعد استنفاد كلّ retry حدّ الطلب / الوظيفة، أو الطبقة التي تبتلعه Warning / Error سجّله مع وحدة الإخفاق
صفّ سيّئ ومتابعة حدّ العنصر Warning اشمل fileId و rowNumber
استثناء غير متوقّع يقتل طلباً كاملاً حدّ الطلب / UI / الوظيفة Error اشمل requestId، و userId، و entityId
شدّة إنهاء العمليّة حدّ الاستثناء غير المعالَج Critical flush، dump، مسار إعادة التشغيل

في الممارسة، أحد أكثر المشاكل شيوعاً هو التسجيل المكرّر مثل:

  • Repository يكتب Error
  • Service يكتب Error لنفس الاستثناء
  • Controller يكتب Error مرّةً أخرى
  • المعالج النهائيّ للاستثناء غير المعالَج يكتب أيضاً Critical

ثمّ ينتج انقطاع واحد عدّة نسخ من نفس stack trace. ما يحتاجه المشغّل فعلاً ليس أربع نسخ من نفس stack trace، بل سجلّ رئيسيّ واحد، وعلى الأكثر عدد قليل من السجلّات الداعمة.

طريقة أخرى لقولها: سجلّ واحد، وقدر ما يلزم من السياق.

7. أنماط مضادّة شائعة

من هنا نرتّب كتابات تظهر فعلاً في المراجعة. لثلاثة منها ألحقنا حدّاً أدنى من كود NG وOK. الكود يفترض C# 10 / .NET 6 فما بعد مع تفعيل الأنواع المرجعيّة القابلة للقيمة الفارغة، ويستعمل System.Text.Json وMicrosoft.Extensions.Logging.

7.1. catch (Exception) في طبقة عميقة تُرجع null / false

هذا يميل إلى محو السبب الحقيقيّ. كما يجعل من المستحيل على المستدعي معرفة ما إذا كانت “البيانات لم تكن موجودة فعلاً” أم “شيء انكسر في المنتصف”.

// NG: 深い層で広く受けて null を返す
private static Order? LoadOrder(string path)
{
    try
    {
        var json = File.ReadAllText(path);
        return JsonSerializer.Deserialize<Order>(json);
    }
    catch (Exception)
    {
        // 呼び出し元からは、ファイルが無かったのか、JSON が壊れていたのか、
        // ディスクが読めなかったのかが区別できない
        return null;
    }
}

هذه الطبقة لا تستطيع حكم كيفيّة معالجة الإخفاق. مرّر الحكم إلى الحدّ، واكتفِ هنا بالترجمة إلى إخفاق ذي معنى.

// この層が投げる、意味のある失敗を表す例外型
public sealed class OrderFileFormatException : Exception
{
    public OrderFileFormatException(string message, Exception? innerException = null)
        : base(message, innerException)
    {
    }
}

// OK: 翻訳だけして、判断は上の境界へ渡す
private static Order LoadOrder(string path)
{
    string json = File.ReadAllText(path);

    try
    {
        return JsonSerializer.Deserialize<Order>(json)
            ?? throw new OrderFileFormatException($"ملف الطلب فارغ: {path}");
    }
    catch (JsonException ex)
    {
        // 下位実装の都合である JsonException を、この層で意味のある失敗へ変える
        throw new OrderFileFormatException($"صيغة ملف الطلب غير صحيحة: {path}", ex);
    }

    // IOException や UnauthorizedAccessException は翻訳せず、そのまま上へ送る。
    // 「ファイルが読めない」は、この層が意味を足せる失敗ではないため
}

7.2. كتابة Error في كلّ طبقة ثمّ إعادة الرمي

هذا أحد أكبر أسباب السجلّات المكرّرة.

إذا قسّمت المسؤوليّات إلى:

  • الطبقات الأدنى تترجم
  • الحدود العليا تكتب السجلّ الرئيسيّ

تنخفض الضوضاء كثيراً.

// NG: 下位層でログしてから再スローする。上位でも同じ例外がログされて 2 本になる
public async Task<Receipt> ChargeAsync(Payment payment, CancellationToken ct)
{
    try
    {
        return await _gateway.ChargeAsync(payment, ct);
    }
    catch (HttpRequestException ex)
    {
        _logger.LogError(ex, "支払いに失敗しました");
        throw;
    }
}

الطبقات الأدنى تترجم وتمرّر فقط.

// PaymentGatewayException は OrderFileFormatException と同じ形の、
// 「支払いサービスとのやり取りが失敗した」ことを表す独自例外型です

// OK: 下位層 (PaymentGateway) は翻訳のみ。ログは出さない
public async Task<Receipt> ChargeAsync(Payment payment, CancellationToken ct)
{
    try
    {
        return await _gateway.ChargeAsync(payment, ct);
    }
    catch (HttpRequestException ex)
    {
        throw new PaymentGatewayException(
            $"支払いサービスへ接続できませんでした。orderId={payment.OrderId}", ex);
    }
}

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

// OK: 境界で主ログを 1 回だけ出し、呼び出し元への応答を決める
[ApiController]
public sealed class PaymentController : ControllerBase
{
    private readonly ILogger<PaymentController> _logger;
    private readonly SaveOrderUseCase _useCase;

    public PaymentController(ILogger<PaymentController> logger, SaveOrderUseCase useCase)
    {
        _logger = logger;
        _useCase = useCase;
    }

    [HttpPost("orders/{orderId}/pay")]
    public async Task<IActionResult> PayAsync(string orderId, CancellationToken ct)
    {
        try
        {
            Receipt receipt = await _useCase.ExecuteAsync(orderId, ct);
            return Ok(receipt);
        }
        catch (PaymentGatewayException ex)
        {
            // وحدة الفشل (دفعة واحدة لهذا الطلب) والسياق التشغيلي لا يلتقيان إلا هنا
            _logger.LogError(ex, "فشل الدفع للطلب {OrderId}", orderId);
            return StatusCode(StatusCodes.Status502BadGateway);
        }
    }
}

في C#، إذا أعدت الرمي، فالشكل الأساسيّ هو throw; حتّى لا تتلف stack trace. كتابة throw ex; تعيد كتابة الـ stack trace عند ذلك السطر، فيضيع موضع الحدوث الحقيقيّ.

7.3. طبقات المكتبات أو المكوّنات المشتركة تعرض UI مباشرةً

إذا فتح مكوّن مشترك MessageBox أو قرّر مباشرةً جسم استجابة HTTP، تنهار كلّ من إعادة الاستخدام وحدود المسؤوليّة. الطبقات الأدنى أكثر أماناً عندما تتوقّف عند إرجاع أو رمي إخفاق ذي معنى.

7.4. تسجيل OperationCanceledException كانقطاع

الإلغاء جزء من تدفّق التحكّم. إذا كتبت Error في كلّ مرّة، تُدفن الإخفاقات الحقيقيّة.

// NG: catch واسع يبتلع الإلغاء أيضاً ويحوّله Error
try
{
    await _useCase.ImportAsync(file, ct);
}
catch (Exception ex)
{
    // حتى ضغط المستخدم «إيقاف» يصل إلى هنا ويظهر Error
    _logger.LogError(ex, "فشل الاستيراد");
    throw;
}

تُقيَّم فقرات catch من الأعلى، لذلك التقط الإلغاء أوّلاً بنوع أكثر تحديداً. جملة when تمنع الخلط بين الإيقاف عبر الرمز الذي مرّرته أنت وOperationCanceledException الناشئ عن مهلة داخليّة أو سبب آخر.

// OK: التقط الإلغاء أولاً وافصله عن سجل العطل
try
{
    await _useCase.ImportAsync(file, ct);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
    // إيقاف المستخدم أو الإغلاق. جزء من تدفّق التحكّم فلا تجعله Error
    _logger.LogInformation("أُوقف الاستيراد. fileId={FileId}", file.Id);
}
catch (Exception ex)
{
    // ما يصل هنا فشل غير متوقَّع فقط. أرفق سياق وحدة الفشل وأخرج السجل الرئيس مرّة واحدة
    _logger.LogError(ex, "فشل الاستيراد. fileId={FileId}", file.Id);
    throw;
}

7.5. retry بخفّة عند وجود آثار جانبيّة خارجيّة

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

7.6. محاولة التعافي من كلّ شيء في المعالج النهائيّ للاستثناء غير المعالَج

ذلك المكان مجرّد بوليصة تأمين أخيرة. لا ينبغي أن يصبح مركز التصميم.

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

8. قائمة فحص للمراجعة

عند مراجعة معالجة الاستثناءات، يساعد المرور بما يلي بالترتيب:

  • هل يمكن شرح هذا catch في جملة واحدة على أنّه القرار الذي يوجد ليتّخذه؟
  • هل يمكن لهذا المكان فعلاً تقرير retry، أو تحويل النتيجة، أو سلوك المتابعة-أو-الإيقاف، أو استجابة المستخدم؟
  • إذا سجّل هنا، فهل سيُسجَّل الإخفاق نفسه أيضاً كـ Error في الأعلى؟
  • هل تُترجَم الاستثناءات الخاصّة بالطبقات الأدنى إلى إخفاقات ذات معنى عند الحدّ؟
  • هل يمكن لهذا المكان استعادة الحالة التالفة؟ إن لم يستطع، فهل التخلّص-وإعادة-الإنشاء هو الافتراض؟
  • هل OperationCanceledException مفصول عن الإخفاق العاديّ؟
  • هل من الواضح ما إذا كانت المتابعة تحدث لكلّ عنصر، لكلّ طلب، أم فقط بعد إعادة تشغيل العمليّة؟
  • هل يُعامَل المعالج النهائيّ للاستثناء غير المعالَج كنقطة تسجيل وليس نقطة تعافٍ؟
  • هل يتضمّن السجلّ سياق وحدة الإخفاق مثل requestId، أو userId، أو batchId، أو fileId، أو rowNumber؟
  • هل تُعامَل “الإخفاقات المتوقّعة” و “الافتراضات المكسورة” بشكل مختلف؟

السؤال الأكثر فاعليّةً بشكل خاصّ هو: “ما الذي يقرّره هذا catch بالضبط؟” إذا لم يمكن الإجابة عن ذلك بوضوح، فإنّ catch غالباً غير ضروريّ أو عميق جدّاً.

9. دليل سريع تقريبيّ

أخيراً نضع جدولاً يطوي الفصلين 3 و6 في ورقة واحدة للتأكيد. يكفي النظر إلى هذه الورقة وحدها عند المراجعة أو عند مراجعة كود انتهيت من كتابته.

الوضع catch التسجيل معالجة الأخطاء
helper / utility عادةً لا لا لا
Repository / Gateway / SDK wrapper التقط فقط استثناءات محدّدة عادةً ليس السجلّ الرئيسيّ الترجمة، retry محلّيّ، التخلّص من الاتّصالات
UseCase / Application Service التقط الإخفاقات المتوقّعة فقط إذا تمّ الابتلاع حسب الحاجة تحويل النتيجة، معالجة الإخفاق الجزئيّ
حدّ UI / Controller / الطلب / العنصر / الوظيفة التقط الاستثناءات غير المتوقّعة على نطاق واسع السجلّ الرئيسيّ الاستجابة، الرسالة، المتابعة / الإلغاء
معالج الاستثناء غير المعالَج فقط ما تسرّب Critical التسجيل النهائيّ، مسار الخروج

عندما تكون غير متأكّد، تكفي هذه القواعد الخمس عادةً:

  1. لا تبتلع على نطاق واسع في الطبقات العميقة
  2. التقط عند الحدود
  3. اكتب السجلّ الرئيسيّ مرّة واحدة
  4. الطبقة التي تبتلع تتحمّل المسؤوليّة
  5. الاستثناء غير المعالَج النهائيّ للتسجيل وتوجيه الخروج

10. الخلاصة

معالجة الاستثناءات ليست قصّة “يمكنك catch في أيّ مكان، لذا يجب أن تستخدم catch في أيّ مكان”.

في الممارسة، يكفي عادةً ترتيب الأسئلة هذا:

  1. هل يمكن لهذا المكان فعلاً اتّخاذ القرار؟
  2. هل وحدة الإخفاق مرئيّة هنا؟
  3. هل يمكن استعادة الحالة أو إعادة بنائها هنا؟
  4. هل سيُنشئ التسجيل هنا تكراراً؟
  5. هل هذه نقطة تعافٍ، أم مجرّد نقطة التسجيل الأخيرة؟

بمجرّد أن تنظر بهذا الترتيب، يصبح تنظيم تسلسل الاستدعاء أسهل بكثير.

الأفكار الثلاث الأكثر أهمّيّة هي هذه:

  • الطبقات العميقة تترجم وتنظّف بشكل رئيسيّ
  • الحدود تقرّر وتكتب السجلّ الرئيسيّ بشكل رئيسيّ
  • المعالج النهائيّ للاستثناء غير المعالَج يسجّل ويوجّه الإنهاء بشكل رئيسيّ

بعبارة مختلفة، يجب التقاط الاستثناءات عند الحدود، وإثراؤها بالسياق، ومعالجتها بالكامل فقط حيث يكون التعافي ممكناً فعلاً.

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

11. المراجع

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

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

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

في أيّ طبقة ينبغي catch للاستثناء؟
المبدأ عدم استعمال catch واسع في الطبقات العميقة، والاقتراب من الحدّ الذي يمكن فيه تعريف وحدة الإخفاق. حدود المعالجة مثل نقرة شاشة واحدة، أو طلب HTTP واحد، أو وظيفة واحدة، أو رسالة واحدة، هي المستقبِل الطبيعيّ. الأساس ليس المكان الذي تستطيع فيه catch، بل المكان الذي تستطيع فيه حكم retry أو التحويل إلى نتيجة أو إمكان الاستمرار بمسؤوليّة. في helper أو utility عميق اكتفِ بالتنظيف في finally، والـ rollback المحلّيّ، وترجمة الاستثناء.
هل ينبغي كتابة سجلّ الاستثناء في كلّ طبقة؟
الأساس سجلّ Error / Critical رئيسيّ واحد لإخفاق واحد. إن كتب Repository Error، ثمّ Service Error للاستثناء نفسه، ثمّ Controller Error مرّة أخرى، اصطفّت عدّة نُسَخ من الـ stack trace نفسه لعطل واحد، فيتحيّر القارئ. الطبقات الأدنى تتوقّف عند الترجمة وإضافة السياق، ويُكتب السجلّ الرئيسيّ عند الحدّ الأعلى حيث يكتمل سياق تشغيليّ مثل requestId وuserId. الطبقة التي تبتلع الاستثناء وتحوّله إلى نتيجة وحدها تحمل مسؤوليّة تسجيل ذلك الإخفاق.
كيف نفصل الإخفاق المتوقّع عن الاستثناء غير المتوقّع؟
الإخفاق المتوقّع إخفاق يمكن تقريره مسبقاً في التصميم؛ نقص التحقّق وNotFound يُحوَّلان إلى نتيجة على وحدة حالة الاستخدام ولا يُجعَلان Error في كلّ مرّة. OperationCanceledException الناتج عن إلغاء المستخدم لا يُعامَل عادةً كـ Error. في المقابل، انهيار افتراض مثل NullReferenceException يُسجَّل سجلّاً رئيسيّاً عند حدّ الطلب / الوظيفة مع استجابة إخفاق، وAccessViolationException أو OutOfMemoryException الشديد يُعامَل كـ Critical ويميل إلى الإنهاء. فصل الاثنين وحدَه يقلّل دفن الإخفاقات الخطرة حقّاً.
ماذا ينبغي أن يفعل معالج الاستثناء غير المعالَج؟
AppDomain.UnhandledException وDispatcherUnhandledException في WPF وThreadException في WinForms ليست نقاط تعافٍ بل آخر موضع للتسجيل. المسؤوليّة الرئيسة السجلّ النهائيّ، وflush، ومسار جمع dump، وترتيب رمز الخروج ومسار إعادة التشغيل. بحلول وصول الاستثناء إلى هنا قد تكون الحالة قد تلفت، والقدرة الظاهرة على الاستمرار لا تعني أنّ الاستمرار آمن. استراتيجيّة التعافي أأمن إن وُضعت في الحدّ السابق: الطلب أو الوظيفة.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

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

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