كانت معالجة الأخطاء كابوساً: كيف أنقذنا نمط ‘Result’ من جحيم الـ try-catch المتشعبة؟

يا هلا بكل المبرمجين والمبرمجات، معكم أخوكم أبو عمر.

بتذكر مرة، قبل كم سنة، كنا شغالين على نظام دفع إلكتروني حساس جداً لأحد العملاء الكبار. كان المشروع ضخم، والـ deadline “على الأبواب”، والضغط النفسي واصل للسما. كان قلبي مقبوض، مش من ضغط الشغل، لأ… من جزء معين في الكود كان عامل زي “الغول” اللي كلنا بنخاف نقرب عليه: وحدة معالجة عملية الدفع.

كانت الوحدة عبارة عن سلسلة من العمليات: التحقق من بيانات المستخدم، ثم التحقق من رصيد البطاقة، ثم التواصل مع بوابة الدفع، ثم حجز المبلغ، ثم تأكيد العملية، ثم إرسال إيصال… وكل خطوة من هدول ممكن تفشل لعشرات الأسباب! زميلنا اللي كتب الكود الأول (الله يسهل عليه) كان مستخدم الـ try-catch بكثافة. مش بس try-catch وحدة، لأ… كانت عبارة عن try-catch جوا try-catch جوا try-catch… إشي بنسميه بالعامية “هرم الهلاك” أو الـ “Pyramid of Doom”.

لما كان يصير خطأ، كنا نقعد ساعات بس عشان نعرف الخطأ صار وين بالضبط! الرسائل كانت عامة “حدث خطأ أثناء الدفع”، والمستخدم والعميل معصّبين، وإحنا مش فاهمين إشي. الوضع صار لا يُطاق. في ليلة من الليالي، وأنا براجع الكود للمرة المليون، تذكرت نمط برمجي كنت قرأت عنه في البرمجة الوظيفية اسمه “Result Pattern”. قلت لحالي: “يا جماعة الخير، ليش ما نجرب إشي جديد؟ ما هو إحنا أصلاً غرقانين”.

وهيك كانت بداية رحلتنا لإنقاذ المشروع، والخروج من جحيم الـ try-catch. خلوني أحكيلكم كيف.

ما هو جحيم الـ try-catch المتشعبة؟

قبل ما نحكي عن الحل، خلينا نفهم المشكلة كويس. لما يكون عندك سلسلة عمليات كل وحدة بتعتمد على اللي قبلها، وكل وحدة ممكن تفشل، الشكل الطبيعي اللي بيلجأ إله المبرمج هو استخدام try-catch. المشكلة بتصير لما تتشعب هاي العمليات.

تخيل معي هذا السيناريو (الكود بلغة C# كمثال، لكن الفكرة تنطبق على أي لغة):


public string ProcessPayment(int userId, decimal amount)
{
    try
    {
        var user = GetUserById(userId); // قد تفشل
        try
        {
            var card = GetUserCreditCard(user); // قد تفشل
            try
            {
                var gateway = new PaymentGateway();
                var transactionId = gateway.Charge(card, amount); // قد تفشل
                try
                {
                    var receipt = SaveReceipt(transactionId, amount); // قد تفشل
                    return "Payment Successful! Receipt ID: " + receipt.Id;
                }
                catch (Exception ex)
                {
                    // فشل في حفظ الإيصال، لازم نعمل rollback
                    gateway.Rollback(transactionId);
                    // Log the error
                    Console.WriteLine("Error saving receipt: " + ex.Message);
                    return "Error: Payment was charged but failed to save receipt.";
                }
            }
            catch (Exception ex)
            {
                // فشل في عملية الدفع نفسها
                Console.WriteLine("Error charging card: " + ex.Message);
                return "Error: Payment failed.";
            }
        }
        catch (Exception ex)
        {
            // فشل في جلب البطاقة
            Console.WriteLine("Error getting credit card: " + ex.Message);
            return "Error: Could not retrieve user's card.";
        }
    }
    catch (Exception ex)
    {
        // فشل في جلب المستخدم
        Console.WriteLine("Error getting user: " + ex.Message);
        return "Error: User not found.";
    }
}

مشاكل هذا النهج

  • صعب القراءة: الكود صار عبارة عن هرم مقلوب. صعب جداً تتبع “المسار السعيد” (Happy Path) وهو المسار اللي كل شي فيه بينجح.
  • فقدان سياق الخطأ: كل catch بتتعامل مع الخطأ بشكل منفصل. لما نوصل للمستخدم النهائي، كل اللي بنعرفه هو “حدث خطأ”. ضاعت كل التفاصيل المهمة.
  • تكرار الكود: لاحظ كيف كود الـ logging والتعامل مع الأخطاء ممكن يتكرر.
  • صعب التعديل: تخيل لو بدنا نضيف خطوة جديدة في النص! بدك تفتح try-catch جديدة وتزيد الطين بلة.

باختصار، هذا الكود غير قابل للصيانة (Unmaintainable) وهش (Brittle). أي تغيير صغير ممكن يكسره.

الحل السحري: نمط النتيجة (Result Pattern)

هنا تدخلت البرمجة الوظيفية لإنقاذ الموقف. فكرة نمط الـ Result بسيطة جداً: بدلاً من رمي استثناء (throw exception) عند حدوث خطأ، الدالة تُرجع كائن (object) يصف النتيجة، سواء كانت نجاحاً أو فشلاً.

هذا الكائن عادةً ما يكون له حالتان:

  1. Success (أو Ok): ويحمل القيمة الناتجة عن العملية الناجحة.
  2. Failure (أو Error): ويحمل معلومات مفصلة عن الخطأ الذي حدث.

هذا يجعل احتمالية الفشل جزءاً صريحاً من “عقد” الدالة (function signature)، بدل ما يكون “قنبلة موقوتة” مخفية ممكن تنفجر بأي لحظة على شكل exception.

كيف نطبّق نمط الـ Result؟

أول شي، بدنا ننشئ كلاس بسيط يمثل هذا النمط. هذا مثال بسيط بلغة C#:


// كلاس أساسي للنتيجة
public class Result
{
    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;
    public Error Error { get; }

    protected Result(bool isSuccess, Error error)
    {
        IsSuccess = isSuccess;
        Error = error;
    }

    public static Result Ok() => new Result(true, Error.None);
    public static Result Ok(T value) => new Result(value, true, Error.None);
    public static Result Fail(Error error) => new Result(false, error);
    public static Result Fail(Error error) => new Result(default, false, error);
}

// كلاس النتيجة مع قيمة (Generic)
public class Result : Result
{
    public T Value { get; }

    protected internal Result(T value, bool isSuccess, Error error)
        : base(isSuccess, error)
    {
        Value = value;
    }
}

// كلاس بسيط لتمثيل الخطأ
public record Error(string Code, string Message)
{
    public static readonly Error None = new Error(string.Empty, string.Empty);
    public static readonly Error NullValue = new Error("Error.Null", "Value was null.");
}

الكود اللي فوق يمكن يبين معقد شوي، بس فكرته بسيطة: عملنا نوع اسمه Result ممكن يحمل قيمة ناجحة (من أي نوع T) أو يحمل خطأ. الأهم هو الدوال المساعدة Ok() و Fail() اللي بتسهل علينا إنشاء هاي الكائنات.

إعادة بناء الكود باستخدام نمط Result

الآن، لنعيد كتابة دوالنا لترجع Result بدلاً من رمي الاستثناءات.


// 1. الدالة الأولى أصبحت ترجع Result<User>
public Result<User> GetUserById(int userId)
{
    var user = _userRepository.Find(userId);
    if (user == null)
    {
        return Result.Fail<User>(new Error("User.NotFound", $"User with ID {userId} not found."));
    }
    return Result.Ok(user);
}

// 2. الدالة الثانية أصبحت ترجع Result<Card>
public Result<Card> GetUserCreditCard(User user)
{
    var card = _cardRepository.GetForUser(user.Id);
    if (card == null || card.IsExpired)
    {
        return Result.Fail<Card>(new Error("Card.Invalid", "User's card is invalid or expired."));
    }
    return Result.Ok(card);
}

// وهكذا... كل الدوال تتبع نفس النمط

لاحظت الفرق؟ الآن، أي مبرمج يقرأ توقيع الدالة public Result<User> GetUserById يعرف فوراً إنها ممكن تفشل، ويعرف نوع القيمة اللي بترجعها في حال النجاح. ما في مفاجآت!

اللحظة السحرية: تجميع العمليات (Chaining)

“طيب يا أبو عمر، هيك صار عندي بدل كل دالة بترمي exception، دالة بترجع object وبدي أعمل if (result.IsSuccess) في كل خطوة. ما حلينا المشكلة، بس غيرنا شكلها!”.

كلامك صحيح 100% لو وقفنا هون. لكن القوة الحقيقية لنمط Result تكمن في قدرته على “التسلسل” أو “التجميع” (Chaining). خلينا نرجع لدالة ProcessPayment ونشوف كيف بتصير “إشي مرتب” بمعنى الكلمة.

سنضيف بعض الدوال المساعدة (Extension Methods) لكلاس Result لتسهيل الربط:


public static class ResultExtensions
{
    // دالة تربط نتيجة بنتيجة أخرى
    public static Result Bind(this Result result, Func<TIn, Result> func)
    {
        if (result.IsFailure)
        {
            return Result.Fail(result.Error);
        }
        return func(result.Value);
    }
    
    // دالة لتنفيذ شي في حالة النجاح
    public static Result Tap(this Result result, Action action)
    {
        if(result.IsSuccess)
        {
            action(result.Value);
        }
        return result;
    }
}

والآن، شاهد السحر وهو يحدث في دالة ProcessPayment الجديدة:


public Result ProcessPayment(int userId, decimal amount)
{
    return GetUserById(userId)
        .Bind(user => GetUserCreditCard(user))
        .Bind(card => ChargeCard(card, amount))
        .Bind(transactionId => SaveReceipt(transactionId, amount))
        .Bind(receipt => CreateSuccessMessage(receipt));
}

// ... دوال مساعدة
private Result ChargeCard(Card card, decimal amount) { /* ... */ }
private Result SaveReceipt(string transactionId, decimal amount) { /* ... */ }
private Result CreateSuccessMessage(Receipt receipt) 
{
    return Result.Ok($"Payment Successful! Receipt ID: {receipt.Id}");
}

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

هذا المفهوم يسمى أحياناً بـ Railway Oriented Programming. تخيل عندك سكتين حديد: سكة النجاح (خضراء) وسكة الفشل (حمراء). الكود ببدأ على السكة الخضراء، وفي كل محطة (دالة Bind)، بنفحص النتيجة. إذا نجحت، بنكمل على نفس السكة الخضراء. إذا فشلت، بنحول القطار فوراً على السكة الحمراء، وبنمشي عليها لنهاية الرحلة بدون ما نمر على أي محطة خضراء ثانية.

نصائح من الخِبرة (من أبو عمر)

  • مش كل خطأ لازم يكون Result: الأخطاء نوعين. النوع الأول هو أخطاء متوقعة (Domain Errors) زي “المستخدم غير موجود” أو “رصيد غير كافي”. هاي مثالية لنمط Result. النوع الثاني هو أخطاء كارثية وغير متوقعة (System Errors) زي “لا يوجد ذاكرة” أو “فشل الاتصال بقاعدة البيانات”. هاي الأخطاء من الأفضل تركها كـ Exceptions، لأنها تعني أن النظام في حالة غير مستقرة ويجب أن يتوقف. لا تستخدم Result لكل شيء.
  • وحّد أنواع الأخطاء: لا تجعل كل دالة ترجع نوع خطأ مختلف. أنشئ مجموعة موحدة من كائنات الأخطاء (Error objects) في مشروعك مثل Errors.Validation, Errors.NotFound, Errors.Permission. هذا يجعل التعامل مع الأخطاء في الطبقات العليا (مثل الـ API Controller) أسهل بكثير.
  • ابدأ صغيراً: لا تحاول إعادة كتابة مشروعك كله مرة واحدة. اختر وحدة (module) معينة تعاني من مشاكل في معالجة الأخطاء وابدأ بتطبيق النمط فيها. عندما يرى الفريق الفائدة، سيتحمس الجميع لتبنيه.
  • الـ Result ليس حلاً لكل المشاكل: هو أداة قوية جداً، لكنه مجرد أداة. استخدمه بحكمة وفي المكان المناسب. أحياناً، جملة if/else بسيطة تكون كافية وأكثر وضوحاً.

الخلاصة… والزبدة

الانتقال من النهج الإجباري (Imperative) المعتمد على try-catch إلى النهج الوظيفي (Functional) المعتمد على Result هو أكثر من مجرد تغيير في الكود، هو تغيير في طريقة التفكير. أنت تجبر نفسك على التفكير في كل مسارات الفشل المحتملة بشكل صريح، مما ينتج عنه كود أكثر متانة (Robust) وموثوقية.

في مشروعنا هذاك، بعد ما طبقنا نمط Result، قلت نسبة الأخطاء الغامضة بشكل هائل، وصار تعديل الكود وإضافة ميزات جديدة متعة بدل ما يكون كابوس. صرنا قادرين نرجع للمستخدم رسائل خطأ دقيقة ومفيدة، وفريق الدعم الفني صار يقدر يحدد المشكلة من أول نظرة على الـ log.

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

بالتوفيق يا جماعة الخير!

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

اختبارات الاداء والجودة

كانت تحديثات CSS تكسر تصميمنا بصمت: كيف أنقذنا ‘الاختبار البصري التراجعي’ من جحيم ‘يبدو مكسورًا’؟

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

2 مايو، 2026 قراءة المزيد
أتمتة العمليات

كانت أوامرنا حبيسة الطرفية (Terminal): كيف حررنا عملياتنا بـ ‘ChatOps’ وجعلناها في متناول الجميع؟

أنا أبو عمر، مبرمج فلسطيني، وأروي لكم كيف انتقلنا من عالم الأوامر المعقدة والمحصورة في الطرفية (Terminal) إلى بيئة عمل شفافة وتعاونية. في هذه المقالة،...

2 مايو، 2026 قراءة المزيد
​معمارية البرمجيات

كانت خدماتنا جزراً معزولة: كيف أنقذتنا ‘المعمارية القائمة على الأحداث’ من جحيم الاقتران المحكم؟

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

2 مايو، 2026 قراءة المزيد
ذكاء اصطناعي

كان بحثنا عن المعنى أعمى: كيف أنقذتنا ‘قواعد بيانات المتجهات’ من جحيم البحث بالكلمات المفتاحية؟

أنا أبو عمر، وفي هذه المقالة سأشارككم قصة حقيقية عن مشروع كاد أن يفشل بسبب البحث التقليدي، وكيف كانت قواعد بيانات المتجهات (Vector Databases) والبحث...

2 مايو، 2026 قراءة المزيد
تسويق رقمي

ميزانيتنا التسويقية كانت ثقباً أسود: كيف أنقذنا ‘نموذج الإحالة المبني على البيانات’ من جحيم إهدار المال؟

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

2 مايو، 2026 قراءة المزيد
البودكاست