معالجة الأخطاء (Error Handling): كيف تجعل نظامك “مضاداً للكسر”.

دقائق القراءة: 7

معالجة الأخطاء (Error Handling): كيف تجعل نظامك “مضاداً للكسر”

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

لهذا السبب، فإن Error Handling ليس طبقة تجميلية في الكود، بل هو العمود الفقري لأي تكامل احترافي. وإذا كنت قد قرأت سابقاً لماذا نحتاج الأتمتة؟ كيف توفر الشركات آلاف الساعات فستعرف أن الأتمتة الحقيقية لا تعني تشغيل المهام فقط، بل تعني تشغيلها بثبات وبقدرة عالية على التعافي.

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

لماذا تفشل أنظمة الأتمتة رغم أن الكود “صحيح”؟

الأنظمة المؤتمتة تعتمد غالباً على خدمات خارجية، وهذا يعني أن موثوقيتها ليست مرتبطة بكودك فقط. قد يكون طلب API مكتوباً بشكل ممتاز، لكن الخدمة المستقبِلة قد تتباطأ، أو ترفض الطلب بسبب Rate Limit، أو ترسل استجابة ناقصة.

ولهذا من المفيد فهم البنية الأساسية للاتصال قبل كتابة طبقة الحماية. يمكنك الرجوع إلى تشريح طلب الـ API: الـ Endpoint، الـ Headers، والـ Body، وكذلك فهم بروتوكول HTTP: رحلة البيانات من جهازك إلى السيرفر لأن معالجة الخطأ تبدأ من معرفة أين يمكن أن ينكسر الطلب أصلاً.

أشهر مصادر الفشل في التكاملات

  • فشل الشبكة أو انقطاع الاتصال قبل وصول الطلب.
  • إرجاع الخادم رموز حالة مثل 500 أو 503.
  • رفض الطلب بسبب الصلاحيات أو انتهاء Bearer Token.
  • إرسال بيانات غير مكتملة داخل JSON Payload.
  • الاعتماد على افتراضات خاطئة حول بنية الاستجابة.
  • تنفيذ نفس العملية مرتين دون حماية من التكرار.

القاعدة الأولى: لا تتعامل مع كل الأخطاء بالطريقة نفسها

من أكبر الأخطاء المعمارية أن نضع كل الاستثناءات داخل try/catch واحد ثم نطبع رسالة عامة. هذا الأسلوب قد يخفي المشكلة الحقيقية، ويمنعك من اتخاذ القرار الصحيح: هل تعيد المحاولة؟ هل تبلغ الفريق؟ هل تتجاهل السجل الحالي وتكمل؟

الأفضل هو تصنيف الأخطاء إلى فئات تشغيلية واضحة:

  1. أخطاء مؤقتة: مثل بطء الشبكة أو 429 Too Many Requests. هذه غالباً تستحق إعادة المحاولة.
  2. أخطاء دائمة: مثل 400 Bad Request الناتجة عن بنية بيانات خاطئة. إعادة المحاولة هنا لا تفيد.
  3. أخطاء منطقية: مثل غياب حقل إلزامي في المصدر أو تكرار تنفيذ نفس الطلب.
  4. أخطاء أمنية: مثل 401 Unauthorized أو 403 Forbidden.

قاعدة عملية: إذا كان الخطأ متعلقاً بالبنية أو التحقق من المدخلات، أصلح البيانات أولاً. وإذا كان الخطأ متعلقاً بالخادم أو الشبكة، استخدم منطق Retry مدروساً. ولتفسير الرموز بشكل أعمق، راجع رموز الحالة (HTTP Status Codes): ماذا يخبرك السيرفر بـ 200 أو 404؟.

بناء طبقة دفاع: التحقق قبل الإرسال وبعد الاستقبال

النظام المضاد للكسر لا ينتظر الانفجار ثم يتصرف، بل يضع نقاط تحقق في بداية ونهاية كل خطوة. قبل إرسال الطلب، تحقق من الحقول المطلوبة ونوعها. وبعد استلام الاستجابة، لا تفترض أن البيانات صحيحة لمجرد أن الخادم أعاد 200.

هذا مهم خصوصاً عند التعامل مع لغة الـ JSON: كيف تقرأ وتكتب البيانات التي تفهمها الآلات، لأن كثيراً من الأعطال تبدأ من حقل مفقود أو قيمة null غير متوقعة.

function validateOrderPayload(payload) {
  if (!payload) throw new Error("Payload is missing");
  if (!payload.customerEmail) throw new Error("customerEmail is required");
  if (!Array.isArray(payload.items) || payload.items.length === 0) {
    throw new Error("items must be a non-empty array");
  }
  return true;
}

function validateApiResponse(data) {
  if (!data || !data.orderId) {
    throw new Error("Invalid API response: orderId not found");
  }
  return data;
}

إعادة المحاولة الذكية أفضل من الإعادة العمياء

استخدام Retry Logic دون ضوابط قد يضاعف الأزمة. إذا كان الخادم متعباً وأنت ترسل عشرات المحاولات فوراً، فأنت تساهم في زيادة الحمل عليه. الحل هو اعتماد Exponential Backoff مع عدد محاولات محدود.

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  let attempt = 0;

  while (attempt <= maxRetries) {
    try {
      const response = await fetch(url, options);

      if (response.status === 429 || response.status >= 500) {
        throw new Error(`Retryable error: ${response.status}`);
      }

      if (!response.ok) {
        throw new Error(`Non-retryable error: ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      if (attempt === maxRetries) throw error;

      const delay = Math.pow(2, attempt) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
      attempt++;
    }
  }
}

هذا النمط مفيد جداً عند العمل مع واجهات تخضع إلى تحديد معدل الطلبات (Rate Limiting): كيف تتجنب الحظر من الخوادم، لأنه يوازن بين الاستمرارية واحترام قدرة الخدمة الخارجية.

لا تعالج الخطأ فقط، بل سجّله بطريقة قابلة للتحليل

إذا فشل التدفق ولم تحتفظ بسياق الفشل، فأنت عملياً لا تملك وسيلة إصلاح فعالة. يجب أن يسجل النظام معلومات كافية عن الحدث: اسم الخطوة، نوع الطلب، معرف العملية، الرمز المرجعي، وقت التنفيذ، وملخص الاستجابة. لكن في المقابل، لا يجب تسجيل الأسرار مثل API Keys أو الرموز الحساسة.

ولهذا يرتبط Error Logging مباشرة بالأمن. راجع مفاتيح الوصول (API Keys): كيف تحمي بابك الخلفي وأمن البيانات: كيفية تخزين المفاتيح السرية في ملفات .env. حتى لا يتحول سجل الأخطاء إلى نقطة تسريب.

function logError(context) {
  const safeLog = {
    step: context.step,
    status: context.status,
    message: context.message,
    requestId: context.requestId,
    timestamp: new Date().toISOString()
  };

  console.error(JSON.stringify(safeLog, null, 2));
}

استخدم التصميم المعماري لتقليل الانهيار المتسلسل

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

هنا نحتاج إلى الفصل بين الوظائف الحرجة والوظائف المساندة. العمليات الأساسية يجب أن تنجح أولاً، ثم تُنفذ المهام الثانوية بشكل غير متزامن عند الإمكان. وإذا كنت تعمل على فرق التكامل بين النداء المباشر والإشعارات الفورية، فراجع الفرق بين الـ API والـ Webhook: “لا تتصل بنا، نحن سنتصل بك”.

ممارسات تجعل التدفق أكثر صلابة

  • استخدم طوابير مهام للعمليات غير الحرجة.
  • افصل خطوة الدفع عن خطوة الإشعار أو الأرشفة.
  • أنشئ Fallback Path عند تعطل مزود خارجي.
  • اعتمد مفاتيح Idempotency لمنع التكرار عند الإعادة.
  • أرسل تنبيهات فورية إلى البريد أو تلجرام عند تجاوز عتبة فشل محددة.

مثال عملي على طلب محمي ضد الفشل

توثيق نقطة اتصال:
POST /api/orders
المدخلات المطلوبة: customerEmail, items
الاستجابات الحرجة المحتملة: 400, 401, 429, 500
الإجراء الموصى به: إعادة المحاولة فقط مع 429 و5xx.

async function createOrder(orderPayload) {
  validateOrderPayload(orderPayload);

  try {
    const data = await fetchWithRetry("https://api.example.com/api/orders", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${process.env.API_TOKEN}`
      },
      body: JSON.stringify(orderPayload)
    });

    return validateApiResponse(data);
  } catch (error) {
    logError({
      step: "createOrder",
      status: "failed",
      message: error.message,
      requestId: orderPayload.requestId || "unknown"
    });
    throw error;
  }
}

اختبار سيناريوهات الفشل جزء من التطوير وليس مرحلة لاحقة

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

وفي البيئات الأكبر، يجب دمج تنبيهات ورقابة تشغيلية مع الجدولة والتنفيذ الدوري، خاصة إذا كانت لديك مهام تعمل عبر الجدولة الزمنية (CRON Jobs): كيف تجعل السكربت يعمل وأنت نائم أو داخل منصات أتمتة مثل مقدمة في منصة Make (Integromat سابقاً): بناء سيناريوهات معقدة واستخدام Pipedream للمبرمجين: دمج Node.js مع الأتمتة.

الخلاصة

النظام المضاد للكسر ليس نظاماً بلا أخطاء، بل نظام يعرف كيف يتوقع الخطأ، ويعزله، ويسجله، ويتعافى منه دون أن يحول العطل الصغير إلى كارثة تشغيلية. كل طبقة في التكامل، من التحقق من المدخلات إلى تفسير رموز HTTP، ومن إعادة المحاولة إلى التسجيل الآمن، تساهم في بناء أتمتة أكثر موثوقية.

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

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *