فلسفة الكود

الاستثناءات بدل مؤشرات الفشل

١٣ أكتوبر ٢٠٢٦ · 5 دقائق قراءة

الفشل الصامت أسوأ من الفشل الصريح

ابدأ بمثال بسيط:

async def find_user(uid):
    user = await db.fetch(uid)
    if not user:
        return None          # الفشل "بينجح" هنا
    return user

المشكلة مش في None — في إنها صامتة. المتصل اللي بياخد القيمة دي ميعرفش إنها نتيجة «مفيش حد» ولا نتيجة «حصل غلط». فبيلبّس الحالتين في نفس القرار:

user = await find_user(uid)
if not user:
    await c.reply("المستخدم محظور")     # غلط كمان لما يكون الراجل مجرد غير نشط

كود كهذا «بينجح» بشكل طبيعي في الإنتاج — لأن الـ None وقع حيث نجاح كاذب. ده أخطر من خطأ صريح يوقّظك أول ما يحصل.

القاعدة اللي طبقناها

مصدر واحد للحقيقة في نوع الخطأ، وأخدنا قرار: المتصل لازم يتبلغ بالطبيعة الحقيقية للحالة عبر استثناءات محددة:

class UserNotFoundError(Exception):
    pass

async def find_user(uid):
    user = await db.fetch(uid)
    if not user:
        raise UserNotFoundError(uid)
    return user

والمعالج المركزي بيترجم كل نوع لرسالة مناسبة قدام المستخدم:

async def error_handler(e, c):
    if isinstance(e, UserNotFoundError):
        return await c.reply("مش لاقيين المستخدم دا")
    return await c.reply("حصل غلط، جرب تاني")

لماذا الاستثناءات المحددة، مش واحدة عامة؟

الاستثناء المحدد بيمرر ليك طبيعة الفشل في سياقه: «المستخدم غير موجود» مختلفة جذريًا عن «الصلاحية ناقصة» — اختلاف في الرد المناسب. لو مسكت بـ except Exception عام، بيروح كل التمييز، ونرجع لنفس الشرط المطاطي اللي كنا بنندهرب منه.

الميزان الوحيد

المستخدم مش بيدي الـ traceback — الـ error handler هو اللي بيحوّله لرسالة. فالموازنة:

  • في الطبقة العميقة: ارفع الاستثناء المحدد، ولا تمسكه من غير قرار.
  • في الطبقة الحدّية (النقل/الواجهة): ترجمه لرسالة بكل وضوح — مكان واحد للفشل المرئي.

دي الدائرة الكاملة: فشل محدد في الأسفل، فشل مُبلّغ في الأعلى، وصفر فشل صامت في المنتصف.