أين يجب التقاط الاستثناءات وتسجيلها ومعالجة الأخطاء - دليل عمليّ للحدود والمسؤوليّات في تسلسل الاستدعاء
· 小村 豪 · معالجة الاستثناءات, التسجيل, معالجة الأخطاء, التصميم, C# / .NET
جدول المحتويات
- الإجابة الموجزة
- الالتقاط والتسجيل ومعالجة الأخطاء أمور مختلفة
- 2.1. الالتقاط
- 2.2. التسجيل
- 2.3. معالجة الأخطاء
- 2.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. النظر إلى تسلسل استدعاء واحد من البداية إلى النهاية
- افصل الإخفاقات المتوقّعة عن الاستثناءات غير المتوقّعة
- أين تسجّل، وكم مرّة
- الأنماط المضادّة الشائعة
- قائمة فحص للمراجعة
- دليل سريع تقريبيّ
- الخلاصة
- المراجع
- مقالات ذات صلة
1. الإجابة الموجزة
- القاعدة هي عدم استخدام
catchالواسع في الطبقات العميقة. ضعcatchبالقرب من الحدّ الذي يمكنك فيه تعريف وحدة الإخفاق. - بالنسبة إلى السجلّات، يجب أن يكون الافتراض سجلّاً رئيسيّاً واحداً لإخفاق واحد. إذا استمرّت كلّ طبقة في كتابة
Errorللاستثناء نفسه، يخسر القارئ. - مسؤوليّة أعمق طبقة هي التنظيف، والـ rollback المحلّي، وترجمة الاستثناء، وretry محدود فقط عند الاقتضاء. إذا أعادت الرمي، فهي عادةً لا تكتب السجلّ الرئيسيّ هناك.
- حدود المعالجة مثل إجراء شاشة واحد، أو طلب HTTP واحد، أو تشغيل وظيفة واحد، أو وحدة معالجة رسالة واحدة، عادةً ما تكون أكثر الأماكن طبيعيّةً للسجلّ الرئيسيّ.
- الإخفاقات المتوقّعة يجب أن تُحوَّل إلى نتائج عند وحدة حالة الاستخدام. لا يلزم أن تستمرّ في رمي كلّ شيء إلى الأعلى كاستثناء إلى الأبد.
AppDomain.UnhandledException، وDispatcherUnhandledExceptionفي WPF، وThreadExceptionفي WinForms، ومعالجات استثناءات ASP.NET Core، والمعالجة النهائيّة لاستثناء المضيف، هي آخر مكان للتسجيل، وليست المكان الرئيسيّ للتعافي.OperationCanceledExceptionالمتعلّق بإلغاء المستخدم أو إيقاف التشغيل عادةً لا يُعامل كـ Error.- عند الشكّ، تحقّق بهذا الترتيب.
- هل يمكن لهذا المكان فعلاً اتّخاذ القرار؟
- هل وحدة الإخفاق مرئيّة هنا؟
- هل يمكن إعادة الحالة أو إعادة بنائها هنا؟
- إذا سجّلت هنا، فهل سيُسجَّل الاستثناء نفسه أيضاً في الأعلى؟
الفكرة الجوهريّة بسيطة: لا تلتقط حيث تستطيع الالتقاط فقط؛ بل التقط حيث يمكنك اتّخاذ قرار مسؤول.
2. الالتقاط والتسجيل ومعالجة الأخطاء أمور مختلفة
2.1. الالتقاط
catch يعني تلقّي استثناء مرّة واحدة وتغيير تدفّق التحكّم بسببه.
لكنّ ذلك بحدّ ذاته ليس تعافياً.
على سبيل المثال، حتّى لو التقطت طريقة أدنى استثناءً، فقد لا تعرف بعد:
- ما الذي يجب عرضه للمستخدم
- هل يجب إيقاف الشاشة كلّها أم يكفي إخفاق هذه العمليّة فقط
- هل يمكن للطلب أو الوظيفة الحاليّة أن تستمرّ
إذا لم تستطع الإجابة عن هذه الأسئلة، فإنّ هذا المكان غالباً ليس مكاناً جيّداً لـ catch.
2.2. التسجيل
التسجيل ليس فقط تدوين أنّ استثناءً قد حدث. بل هو تدوين أيّ جزء من العمل أخفق حتّى تستطيع تتبّعه لاحقاً.
لذلك تحتوي نقاط التسجيل الجيّدة عادةً على واحد أو أكثر ممّا يلي:
- requestId / traceId
- userId
- orderId / fileId / batchId
- أيّ عنصر إدخال أخفق
- أيّ إجراء UI كان
- أيّ قائمة انتظار أو أيّ رسالة كانت
المساعدات العميقة والوظائف المشتركة غالباً تعرف التفاصيل التقنيّة لكن لا تملك هذا السياق التشغيليّ. لذلك فإنّ المكان الذي يعرف التفاصيل التقنيّة و المكان الذي يعرف السياق التشغيليّ غالباً ما يكونان مكانين مختلفين.
2.3. معالجة الأخطاء
هنا، تعني “معالجة الأخطاء” أشياء مثل:
- عرض رسالة خطأ على الشاشة
- إعادة 4xx / 5xx في HTTP
- إخفاق عنصر واحد فقط والمتابعة إلى التالي
- إعادة تهيئة نظام فرعيّ
- إيقاف العمليّة وترك سياسة إعادة التشغيل تتولّى
- تحرير الموارد والخروج بأمان
بعبارة أخرى، تعني تحديد الشكل المرئيّ للإخفاق بالنسبة إلى المستدعي أو المستخدم.
2.4. ترجمة الاستثناءات
في الأنظمة الحقيقيّة، توجد خطوة مهمّة أخرى بين catch و”معالجته”.
هذه الخطوة هي الترجمة.
على سبيل المثال:
HttpRequestExceptionIOExceptionJsonException- الاستثناءات الخاصّة ببرنامج تشغيل قاعدة البيانات
- الاستثناءات الخاصّة بـ vendor SDK
إذا تسرّبت تلك مباشرةً إلى UI أو Controller، تبدأ الطبقات العليا بتعلّم تفاصيل تنفيذ الطبقات الأدنى.
لذلك عند الحدّ، من الأفضل غالباً ترجمتها إلى إخفاقات ذات معنى في تلك الطبقة، مثل:
- “تعذّر الاتّصال بخدمة الدفع”
- “كان تنسيق CSV غير صالح”
- “تعذّر الكتابة إلى وجهة الحفظ”
- “كانت استجابة الجهاز غير صالحة”
النقطة الأساسيّة هي أنّ الترجمة ليست الشيء نفسه كالتسجيل. إذا قمت بالترجمة فقط وأعدت الرمي، فإنّ ذلك المكان عادةً ليس مكان السجلّ الرئيسيّ.
3. جدول القرار الأوّل الذي ننظر إليه
يصبح البقاء منظّماً أسهل بكثير إذا قرّرت أوّلاً السياسة العامّة بجدول كهذا.
| المكان | السياسة الأساسيّة | السجلّ الرئيسيّ | المسؤوليّة الرئيسيّة |
|---|---|---|---|
| helper / utility / private method | كقاعدة، لا تستخدم catch على نطاق واسع |
لا شيء | التنظيف في finally، rollback محلّي، إضافة الحدّ الأدنى من السياق |
| Repository / Gateway / SDK wrapper | التقط فقط استثناءات محدّدة | عادةً لا | ترجمة الاستثناءات، retry محدود، التخلّص من الاتّصالات أو المقابض المكسورة |
| Application Service / UseCase | حوّل الإخفاقات المتوقّعة إلى نتائج | إذا تمّ ابتلاعه، فقط حسب الحاجة هنا | تعريف وحدة الإخفاق، السماح بالإخفاق الجزئيّ، اتّخاذ قرارات على مستوى حالة الاستخدام |
| حدّ UI / Controller / API / Job / Message | المستقبل الرئيسيّ للاستثناءات غير المتوقّعة | غالباً هنا | استجابة المستخدم، استجابة HTTP، قرار المتابعة-إلى-التالي أو الإلغاء |
| معالج الاستثناء غير المعالَج / الحدّ النهائيّ للمضيف | خطّ الدفاع الأخير للحالات المفقودة | Critical |
التسجيل النهائيّ، flush، dump، مسار الخروج / إعادة التشغيل |
بصريّاً، يبدو الأمر عادةً كالتالي:
flowchart TD
A["An exception happened"] --> B{"Can this place decide retry / result conversion / continue-or-stop?"}
B -- "No" --> C["As a rule, do not catch it here; send it upward"]
B -- "Yes" --> D{"Is this a layer boundary?"}
D -- "No" --> E["Only local cleanup"]
D -- "Yes" --> F["Translate into a meaningful exception if needed"]
E --> G{"Can the unit of failure and operational context be identified here?"}
F --> G
G -- "No" --> H["Do not write the primary log here; send upward"]
G -- "Yes" --> I["Write one primary log and decide the response"]
I --> J["Exit / reinitialize / continue next item if needed"]
توجد فكرتان أساسيّتان في هذا الرسم.
- السبب الأوّل لـ
catchهو التعافي أو التنظيف، وليس التسجيل - السبب الأوّل للتسجيل هو اكتمال السياق التشغيليّ، وليس مجرّد ملاحظتك لاستثناء
4. ماذا نفعل في كلّ مستوى من تسلسل الاستدعاء
4.1. أعمق helper / utility / private method
في هذا المستوى، القاعدة الافتراضيّة هي عدم الالتقاط على نطاق واسع.
في أماكن مثل تحويل النصوص، أو الـ parsing، أو الحسابات، أو التنسيق الداخليّ، أو المساعدات المشتركة، عادةً لا يستطيع الكود أن يقرّر:
- أيّ إجراء شاشة كان ينتمي إليه
- أيّ طلب كان ينتمي إليه
- هل يجب أن تخفق هذه العمليّة فقط
- هل يجب إيقاف الشاشة كلّها
ما يُسمح لهذه الطبقة بفعله هو في الغالب:
- تحرير الموارد في
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
- استثناءات الـ parser أو الـ serializer
المسؤوليّات النموذجيّة في هذه الطبقة هي هذه الأربعة:
-
التقط استثناءات محدّدة التقط استثناءات محدّدة ذات معنى بدلاً من
Exceptionواسع. -
ترجمها إلى إخفاقات ذات معنى حتّى لا تحتاج الطبقات العليا إلى معرفة تفاصيل تنفيذ الطبقات الأدنى مباشرةً.
- إذا كان retry المحلّي مناسباً، فافعله هنا
لكن فقط ضمن شروط صارمة نوعاً ما:
- من المعروف أنّ الإخفاق عابر
- العمليّة idempotent
- عدد الـ retry وسياسة التأخير مُعرَّفة
- السلوك النهائيّ بعد الإخفاق واضح ينتمي retry إلى هنا فقط عند استيفاء هذه الشروط.
- تخلّص من الاتّصالات أو المقابض المكسورة في كثير من الحالات، “إعادة إنشاء الاتّصال” أكثر أماناً من “الاستمرار في استخدام نفس الكائن في المرّة القادمة”.
بالنسبة إلى التسجيل في هذه الطبقة، تساعد السياسة التالية في تجنّب الالتباس:
- إذا أعدت الرمي إلى الأعلى، فعادةً لا تكتب السجلّ الرئيسيّ هنا
- إذا ابتلعت هذه الطبقة الاستثناء وحوّلته إلى نتيجة، فإنّ هذه الطبقة تمتلك السجلّ أو المقياس اللازم
- أثناء 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. النظر إلى تسلسل استدعاء واحد من البداية إلى النهاية
على سبيل المثال، تخيّل تدفّقاً كهذا:
flowchart LR
A["UI / Controller / Job boundary"] --> B["Application Service / UseCase"]
B --> C["Domain / business logic"]
C --> D["Repository / Gateway / SDK wrapper"]
D --> E["DB / HTTP / File / Vendor SDK"]
في تلك الحالة، تنفصل الأدوار عادةً تقريباً كالتالي.
زرّ الحفظ → SaveOrderUseCase → PaymentGateway → HTTP
PaymentGateway- يلتقط إخفاقات الاتّصال وتنسيقات الاستجابة غير الصالحة
- يترجمها إلى شيء مثل “إخفاق اتّصال خدمة الدفع” أو “استجابة خدمة دفع غير صالحة”
- يُجري retry هنا فقط عندما تبرّر الشروطُ ذلك
- إذا أعاد الرمي، فهو عادةً لا يكتب السجلّ الرئيسيّ
SaveOrderUseCase- يحوّل الإخفاقات المتوقّعة مثل رفض الدفع إلى نتائج
- يعامل الإخفاق كـ “إنهاء هذا الطلب فقط أخفق”
- يشكّل الإخفاق حتّى تستطيع طبقات UI أو API إرجاعه بنظافة
- معالج زرّ UI / Controller
- يلتقط الاستثناءات غير المتوقّعة على نطاق واسع
- يكتب السجلّ الرئيسيّ مع
orderId، وuserId، وrequestId - يحوّل الإخفاق إلى مربّع حوار، أو 500، أو استجابة 503
- معالج الاستثناء غير المعالَج
- يسجّل فقط ما تسرّب إلى هذا الحدّ
- يُجري جمع dump أو flush نهائيّاً
- يعطي الأولويّة لمسار الخروج بدلاً من التعافي
مع هذا التقسيم، تظلّ التفاصيل التقنيّة مغلقةً في الأسفل، ويُلصق السياق التشغيليّ في الأعلى، وتُتّخذ القرارات عند الحدود.
5. افصل الإخفاقات المتوقّعة عن الاستثناءات غير المتوقّعة
أهمّ شيء في هذا الموضوع كلّه هو عدم معاملة كلّ شيء كنفس النوع من “الاستثناء”.
يساعد فصلها تقريباً كالتالي:
| نوع الإخفاق | المكان الأوّل لمعالجته | المعالجة النموذجيّة |
|---|---|---|
| مشكلة تحقّق من الصحّة | UseCase / حدّ الطلب | إعادتها كخطأ إدخال |
NotFound / Conflict |
UseCase / Controller | 404 / 409 أو رسالة شاشة |
| إلغاء المستخدم / إيقاف التشغيل | حدّ العمليّة | إلغاء؛ عادةً ليس Error |
| صفّ CSV واحد غير صالح | حدّ العنصر الواحد | تسجيله كـ Warning ومتابعة |
| timeout عابر ينتهي مع ذلك بإخفاق | من حدّ I/O إلى حدّ الطلب | الإخفاق بعد retry |
NullReferenceException، افتراضات مكسورة |
حدّ الطلب / الوظيفة | السجلّ الرئيسيّ واستجابة الإخفاق |
AccessViolationException، OutOfMemoryException شديد، أو رائحة فساد على حدّ native |
الحدّ النهائيّ | معاملته كـ Critical والانتقال نحو إيقاف التشغيل |
الإخفاقات المتوقّعة هي إخفاقات يمكنك التصميم لها مسبقاً. الاستثناءات غير المتوقّعة هي إخفاقات يكون من المشكوك فيه ما إذا كان لا يزال يجب الوثوق بالحالة بعدها.
فصل هذين الاثنين فقط يقلّل من مشاكل مثل:
- تسجيل
NotFoundكـErrorفي كلّ مرّة - معاملة إلغاء المستخدم كانقطاع
- السماح للإخفاقات الخطيرة فعلاً للافتراضات المكسورة بالاستمرار كما لو كانت مجرّد “أخفق هذا الطلب فقط”
6. أين تسجّل، وكم مرّة
عند تصميم السجلّات، غالباً ما يكون قرار من يملك السجلّ الرئيسيّ أهمّ من قرار المكان الدقيق لـ catch.
القواعد الأساسيّة هي:
- سجلّ
Error/Criticalرئيسيّ واحد لإخفاق واحد - الطبقات الأدنى تضيف الترجمة والسياق فقط عند الحاجة
- الحدود العليا تكتب السجلّ الرئيسيّ مع وحدة الإخفاق والسياق التشغيليّ
- فقط الطبقة التي تبتلع الإخفاق تتحمّل المسؤوليّة الكاملة عن تسجيل ذلك الإخفاق المبتلع
- الإخفاقات المتوقّعة لا ينبغي أن تصبح دائماً
Error OperationCanceledExceptionيجب فصله عن سجلّات الإخفاق العاديّة
جدول تقريبيّ لنقاط التسجيل يبدو كالتالي:
| الوضع | المكان الرئيسيّ للتسجيل | المستوى النموذجيّ | ملاحظة |
|---|---|---|---|
| خطأ تحقّق من الصحّة | حدّ الطلب / حالة الاستخدام | 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. الأنماط المضادّة الشائعة
7.1. catch (Exception) في طبقة عميقة تُرجع null / false
هذا يميل إلى محو السبب الحقيقيّ. كما يجعل من المستحيل على المستدعي معرفة ما إذا كانت “البيانات لم تكن موجودة فعلاً” أم “شيء انكسر في المنتصف”.
7.2. كتابة Error في كلّ طبقة ثمّ إعادة الرمي
هذا أحد أكبر أسباب السجلّات المكرّرة.
إذا قسّمت المسؤوليّات إلى:
- الطبقات الأدنى تترجم
- الحدود العليا تكتب السجلّ الرئيسيّ
تنخفض الضوضاء كثيراً.
في C#، إذا أعدت الرمي، فالشكل الأساسيّ هو throw; حتّى لا تتلف stack trace.
7.3. طبقات المكتبات أو المكوّنات المشتركة تعرض UI مباشرةً
إذا فتح مكوّن مشترك MessageBox أو قرّر مباشرةً جسم استجابة HTTP، تنهار كلّ من إعادة الاستخدام وحدود المسؤوليّة.
الطبقات الأدنى أكثر أماناً عندما تتوقّف عند إرجاع أو رمي إخفاق ذي معنى.
7.4. تسجيل OperationCanceledException كانقطاع
الإلغاء جزء من تدفّق التحكّم.
إذا كتبت Error في كلّ مرّة، تُدفن الإخفاقات الحقيقيّة.
7.5. retry بخفّة عند وجود آثار جانبيّة خارجيّة
بالنسبة إلى أمور مثل إرسال البريد الإلكترونيّ، أو الفوترة، أو أوامر الجهاز، أو نقل الملفّات، فإنّ إجراء العمليّة نفسها مرّةً أخرى يمكن أن يسبّب ضرراً بسهولة. ينتمي retry فقط حيث يكون كلّ من الإخفاق العابر و idempotency مرئيّاً.
7.6. محاولة التعافي من كلّ شيء في المعالج النهائيّ للاستثناء غير المعالَج
ذلك المكان مجرّد بوليصة تأمين أخيرة. لا ينبغي أن يصبح مركز التصميم.
استراتيجيّة التعافي عادةً ما تكون أكثر أماناً عندما تعيش خطوةً واحدةً قبلها، عند حدّ الطلب / الوظيفة / النظام الفرعيّ.
8. قائمة فحص للمراجعة
عند مراجعة معالجة الاستثناءات، يساعد المرور بما يلي بالترتيب:
- هل يمكن شرح هذا
catchفي جملة واحدة على أنّه القرار الذي يوجد ليتّخذه؟ - هل يمكن لهذا المكان فعلاً تقرير retry، أو تحويل النتيجة، أو سلوك المتابعة-أو-الإيقاف، أو استجابة المستخدم؟
- إذا سجّل هنا، فهل سيُسجَّل الإخفاق نفسه أيضاً كـ
Errorفي الأعلى؟ - هل تُترجَم الاستثناءات الخاصّة بالطبقات الأدنى إلى إخفاقات ذات معنى عند الحدّ؟
- هل يمكن لهذا المكان استعادة الحالة التالفة؟ إن لم يستطع، فهل التخلّص-وإعادة-الإنشاء هو الافتراض؟
- هل
OperationCanceledExceptionمفصول عن الإخفاق العاديّ؟ - هل من الواضح ما إذا كانت المتابعة تحدث لكلّ عنصر، لكلّ طلب، أم فقط بعد إعادة تشغيل العمليّة؟
- هل يُعامَل المعالج النهائيّ للاستثناء غير المعالَج كنقطة تسجيل وليس نقطة تعافٍ؟
- هل يتضمّن السجلّ سياق وحدة الإخفاق مثل requestId، أو userId، أو batchId، أو fileId، أو rowNumber؟
- هل تُعامَل “الإخفاقات المتوقّعة” و “الافتراضات المكسورة” بشكل مختلف؟
السؤال الأكثر فاعليّةً بشكل خاصّ هو: “ما الذي يقرّره هذا catch بالضبط؟”
إذا لم يمكن الإجابة عن ذلك بوضوح، فإنّ catch غالباً غير ضروريّ أو عميق جدّاً.
9. دليل سريع تقريبيّ
أخيراً، في شكل أقصر بكثير، يبدو التقسيم عادةً كالتالي:
| الوضع | catch |
التسجيل | معالجة الأخطاء |
|---|---|---|---|
| helper / utility | عادةً لا | لا | لا |
| Repository / Gateway / SDK wrapper | التقط فقط استثناءات محدّدة | عادةً ليس السجلّ الرئيسيّ | الترجمة، retry محلّي، التخلّص من الاتّصالات |
| UseCase / Application Service | التقط الإخفاقات المتوقّعة | فقط إذا تمّ الابتلاع حسب الحاجة | تحويل النتيجة، معالجة الإخفاق الجزئيّ |
| حدّ UI / Controller / الطلب / العنصر / الوظيفة | التقط الاستثناءات غير المتوقّعة على نطاق واسع | السجلّ الرئيسيّ | الاستجابة، الرسالة، المتابعة / الإلغاء |
| معالج الاستثناء غير المعالَج | فقط ما تسرّب | Critical |
التسجيل النهائيّ، مسار الخروج |
عندما تكون غير متأكّد، تكفي هذه القواعد الخمس عادةً:
- لا تبتلع على نطاق واسع في الطبقات العميقة
- التقط عند الحدود
- اكتب السجلّ الرئيسيّ مرّة واحدة
- الطبقة التي تبتلع تتحمّل المسؤوليّة
- الاستثناء غير المعالَج النهائيّ للتسجيل وتوجيه الخروج
10. الخلاصة
معالجة الاستثناءات ليست قصّة “يمكنك catch في أيّ مكان، لذا يجب أن تستخدم catch في أيّ مكان”.
في الممارسة، يكفي عادةً ترتيب الأسئلة هذا:
- هل يمكن لهذا المكان فعلاً اتّخاذ القرار؟
- هل وحدة الإخفاق مرئيّة هنا؟
- هل يمكن استعادة الحالة أو إعادة بنائها هنا؟
- هل سيُنشئ التسجيل هنا تكراراً؟
- هل هذه نقطة تعافٍ، أم مجرّد نقطة التسجيل الأخيرة؟
بمجرّد أن تنظر بهذا الترتيب، يصبح تنظيم تسلسل الاستدعاء أسهل بكثير.
الأفكار الثلاث الأكثر أهمّيّة هي هذه:
- الطبقات العميقة تترجم وتنظّف بشكل رئيسيّ
- الحدود تقرّر وتكتب السجلّ الرئيسيّ بشكل رئيسيّ
- المعالج النهائيّ للاستثناء غير المعالَج يسجّل ويوجّه الإنهاء بشكل رئيسيّ
بعبارة مختلفة، يجب التقاط الاستثناءات عند الحدود، وإثراؤها بالسياق، ومعالجتها بالكامل فقط حيث يكون التعافي ممكناً فعلاً.
بمجرّد اتّخاذ هذا القرار، تصبح كلّ من مراجعات الكود وتحقيقات الحوادث أقلّ تذبذباً بكثير.
11. المراجع
- .NET: Best practices for exceptions
- .NET: System.AppDomain.UnhandledException event
- WPF: Application.DispatcherUnhandledException event
- Windows Forms: Application.ThreadException event
- Handle errors in ASP.NET Core
- ASP.NET Core middleware
- Use BackgroundService to create Windows Services
12. مقالات ذات صلة
- قائمة فحص للاستثناءات غير المتوقّعة - هل يجب أن يخرج التطبيق أم يستمرّ؟ جدول قرار عمليّ
- إذا اضطررت إلى بناء logger خاصّ بك، فما الحدّ الأدنى الذي تحتاجه فعلاً؟ - متطلّبات عمليّة وفحوص اختبار التكامل
- ما هو .NET Generic Host - شرح DI والتكوين والتسجيل و BackgroundService
- أين تنتهي اختبارات الوحدة وأين تبدأ اختبارات التكامل - دليل عمليّ للحدود
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
قائمة تحقّق للحدّ الأدنى من الأمان في تطوير تطبيقات Windows
قائمة تحقّق عمليّة لخطّ الأساس الأمنيّ في تطبيقات Windows: حدود الصلاحيّات، توقيع التوزيع، حماية الأسرار، HTTPS، تحميل DLL، logging، وتحد...
كيف نُبقي سجلّات الأعطال في تطبيقات Windows حتى عند الموت بسبب أخطاء برمجية - أفضل الممارسات مع WER وعلامات نهائية وتصميم watchdog
يشرح هذا المقال كيف نضمن الحصول على أدلّة قابلة للتشخيص حتى حين تموت عملية Windows بخطأ برمجي حقيقي، عبر دمج السجلّات الزمنية وعلامة cras...
checklist للاستثناءات غير المتوقّعة - هل يجب أن يخرج التطبيق أم يستمرّ؟ جدول قرار عمليّ
جدول قرار عمليّ يساعدك على الحكم بعد استثناء غير متوقّع في Windows هل يخرج التطبيق أم يستمرّ، عبر فحص تلف الحالة والآثار الجانبيّة وحدود ...
كيف تعزل العمل الذي يحتاج إلى المسؤول فقط داخل تطبيقات Windows
دليل عمليّ لإبقاء واجهة تطبيق Windows عند asInvoker مع إسناد العمل المرفَّع إلى helper EXE عبر runas و named pipes، مع التحقّق من الطلبات...
أفضل الممارسات في DPAPI لإبعاد الأسرار عن إعدادات النصّ الصريح في تطبيقات Windows
دليل تطبيقيّ لاستخدام DPAPI و ProtectedData في .NET لتخزين أسرار تطبيقات Windows محلّيّاً بدل النصّ الصريح، مع اختيار CurrentUser و Local...
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.
روابط عامة