البرمجة متوسط 9 دقيقة

كيفية التعامل مع الأخطاء مركزياً في تطبيق Express

كتلات try/catch المتفرقة التي تُنسّق استجابات الأخطاء كل منها على حدة تمثّل عبئاً على الصيانة وتُنتج مخرجات غير متسقة حتماً. مركزة معالجة الأخطاء في middleware واحد يمنحك مكاناً واحداً للتنسيق والتسجيل وتحديد ما تكشفه — دون خطر تسريب stack trace في بيئة الإنتاج.

الخطوات

  1. 1

    تعريف فئة HttpError بنوع محدد

    قبل كتابة الـ middleware، أنشئ فئة خطأ صغيرة تحمل رمز حالة HTTP. رمي هذه الفئة بدلاً من Error العادية يتيح للمعالج المركزي التمييز بين الأخطاء المقصودة والأخطاء البرمجية.

    javascript
    // errors/HttpError.js
    class HttpError extends Error {
      constructor(status, code, message) {
        super(message);
        this.name = 'HttpError';
        this.status = status;
        this.code = code;
      }
    }
    
    HttpError.badRequest   = (msg) => new HttpError(400, 'BAD_REQUEST', msg);
    HttpError.unauthorized = (msg) => new HttpError(401, 'UNAUTHORIZED', msg ?? 'Authentication required');
    HttpError.forbidden    = (msg) => new HttpError(403, 'FORBIDDEN', msg ?? 'Access denied');
    HttpError.notFound     = (msg) => new HttpError(404, 'NOT_FOUND', msg ?? 'Resource not found');
    
    module.exports = HttpError;
  2. 2

    كتابة middleware مركزي لمعالجة الأخطاء

    يُعرّف Express الـ middleware للأخطاء من خلال توقيعه ذي الأربعة وسائط (err, req, res, next). يجب تسجيله بعد جميع المسارات والـ middleware الأخرى.

    javascript
    // middleware/errorHandler.js
    const HttpError = require('../errors/HttpError');
    const logger = require('../logger');
    
    function errorHandler(err, req, res, next) {
      const isOperational = err instanceof HttpError;
    
      const status  = isOperational ? err.status : 500;
      const code    = isOperational ? err.code   : 'INTERNAL_ERROR';
      const message = isOperational ? err.message : 'حدث خطأ غير متوقع';
    
      if (!isOperational) {
        logger.error({ err, req: { method: req.method, url: req.url } }, 'خطأ غير معالج');
      } else {
        logger.warn({ code, status, url: req.url }, err.message);
      }
    
      res.status(status).json({ error: { code, message } });
    }
    
    module.exports = errorHandler;
  3. 3

    تسجيل المعالج آخراً في app.js

    لن يستدعي Express الـ middleware للأخطاء إلا إذا مرّر middleware أو مسار سابق خطأً إلى next(err)، أو رمى خطأً بشكل متزامن. سجّله كآخر استدعاء لـ app.use.

    javascript
    const express = require('express');
    const errorHandler = require('./middleware/errorHandler');
    const userRoutes   = require('./routes/users');
    
    const app = express();
    app.use(express.json());
    
    app.use('/api/users', userRoutes);
    
    // يجب أن يكون آخر شيء
    app.use(errorHandler);
    
    module.exports = app;
  4. 4

    تغليف معالجات المسارات غير المتزامنة

    لا يعترض Express 4 رفض الـ Promises تلقائياً. الرفض غير المعالج يُسكت الطلب بصمت. غلّف كل معالج غير متزامن بأداة صغيرة لإعادة توجيه الأخطاء إلى الـ middleware المركزي.

    javascript
    // utils/asyncHandler.js
    const asyncHandler = (fn) => (req, res, next) =>
      Promise.resolve(fn(req, res, next)).catch(next);
    
    module.exports = asyncHandler;
    
    // الاستخدام في ملف المسارات:
    const asyncHandler = require('../utils/asyncHandler');
    const HttpError = require('../errors/HttpError');
    
    router.get('/:id', asyncHandler(async (req, res) => {
      const user = await User.findById(req.params.id);
      if (!user) throw HttpError.notFound('المستخدم غير موجود');
      res.json(user);
    }));
  5. 5

    الترقية إلى Express 5 للدعم الأصلي للـ async

    يعترض Express 5 (صدر عام 2024) الـ Promises المرفوضة من معالجات المسارات تلقائياً، فلا حاجة لـ asyncHandler بعد الآن. إن كنت تبدأ مشروعاً جديداً، استخدم Express 5.

    javascript
    npm install express@5
    
    // Express 5 — لا حاجة لـ asyncHandler
    router.get('/:id', async (req, res) => {
      const user = await User.findById(req.params.id);
      if (!user) throw HttpError.notFound('المستخدم غير موجود');
      res.json(user);
    });
  6. 6

    التمييز بين الأخطاء التشغيلية وأخطاء المبرمج

    هذا التمييز مهم لعمليات الإنجاز. الخطأ التشغيلي (مستخدم غير موجود، فشل التحقق) متوقع وآمن للعرض للعميل. خطأ المبرمج (لا يمكن قراءة خاصية من undefined) يُشير إلى خلل — يجب أن يُطلق تنبيهاً، ويُرسل stack trace كاملاً إلى نظام التسجيل، بينما تكون الاستجابة للعميل 500 عاماً دون تفاصيل.

    javascript
    // في المعالج المركزي:
    if (!isOperational) {
      logger.error({ err }, 'خطأ برمجي — تنبيه المناوب');
      // في بعض الإعدادات يُفضَّل إعادة تشغيل العملية:
      // process.exit(1); // سيُعيد تشغيلها مدير العمليات (PM2 أو systemd)
    }
  7. 7

    إعادة شكل موحد لاستجابات الأخطاء

    يجب أن تبدو كل استجابة خطأ من واجهة API بنفس الشكل. لا ينبغي للعملاء التخمين إن كان الخطأ في err أو error أو message أو errors[0]. اتفق على شكل واحد وطبّقه حصراً من خلال المعالج المركزي.

    json
    // كل استجابة خطأ من واجهة API:
    {
      "error": {
        "code": "NOT_FOUND",
        "message": "المستخدم غير موجود"
      }
    }
    
    // أخطاء التحقق يمكن توسيع الشكل:
    {
      "error": {
        "code": "VALIDATION_ERROR",
        "message": "فشل التحقق من الطلب",
        "fields": {
          "email": "يجب أن يكون عنوان بريد إلكتروني صحيح",
          "age":   "يجب أن يكون عدداً صحيحاً موجباً"
        }
      }
    }
  8. 8

    لا تُسرّب Stack Trace في الإنتاج أبداً

    تكشف Stack Traces عن بنية ملفاتك وإصدارات المكتبات وأحياناً أسماء المتغيرات — وكلها مفيدة للمهاجمين. يجب أن يتضمن المعالج المركزي Stack Trace في الاستجابة فقط عندما تكون البيئة مضبوطة صراحةً على development.

    javascript
    function errorHandler(err, req, res, next) {
      const isOperational = err instanceof HttpError;
      const status  = isOperational ? err.status : 500;
      const code    = isOperational ? err.code   : 'INTERNAL_ERROR';
      const message = isOperational ? err.message : 'حدث خطأ غير متوقع';
    
      const body = { error: { code, message } };
    
      // Stack Trace في بيئة التطوير فقط
      if (process.env.NODE_ENV === 'development') {
        body.error.stack = err.stack;
      }
    
      res.status(status).json(body);
    }

نصائح ومحاذير

  • استخدم <code>pino</code> أو <code>winston</code> لتسجيل JSON منظّم — لا يمكن البحث في <code>console.error</code> العادي في أنظمة تجميع السجلات في الإنتاج.
  • استدعِ دائماً <code>next(err)</code> داخل كتلة <code>catch</code> بدلاً من استدعاء <code>res.json()</code> مباشرةً — التوجيه عبر المعالج المركزي يُبقي التنسيق متسقاً.
  • أضف مساراً شاملاً (<code>app.use((req, res) => res.status(404).json(...))</code>) قبل معالج الأخطاء لإنتاج استجابات 404 نظيفة للمسارات غير المعروفة.
  • لا ترمِ خطأً داخل معالج الأخطاء نفسه — إن فعلت، لا يوجد لدى Express بديل وسيتوقف الطلب.

خاتمة

middleware واحد للأخطاء، وفئة خطأ محددة النوع، وغلاف للـ async — هذا كل ما تحتاجه للتعامل مع الأخطاء بشكل نظيف عبر تطبيق Express بأكمله. أضف تسجيلاً منظّماً وشكلاً موحداً للاستجابات، وسيصبح تطبيقك أسهل بكثير في التصحيح والمراقبة والصيانة.

#Node.js #Express #Errors
العودة إلى جميع الأدلة

هل تحتاج مساعدة في مشروعك؟

احجز استشارة مجانية لمدة 30 دقيقة لمناقشة تحدياتك التقنية واستكشاف الحلول معًا.