Kodax

معالجة الأخطاء Error Handling — REST APIs

معالجة الأخطاء (Error Handling)

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

نوعان من الأخطاء

| النوع | أمثلة | الاستجابة |

|---|---|---|

| تشغيلية (متوقعة) | مدخل غير صالح، غير موجود، غير مصرح | 4xx مع رسالة واضحة |

| برمجية (غير متوقعة) | undefined is not a function، قاعدة البيانات متوقفة | 500 رسالة عامة + تسجيل كامل للمطور |

صنف خطأ مخصص

class AppError extends Error {
  constructor(message, status = 500, code = "INTERNAL") {
    super(message);
    this.status = status;
    this.code = code;
    this.isOperational = status < 500;
  }
}
const notFound = (what) => new AppError(`${what} غير موجود`, 404, "NOT_FOUND");
console.log(notFound("الكتاب").status);

أخطاء الدوال async

- Express 5: إذا رمت دالة async أو رُفض Promise، يُمرر الخطأ لمعالج الأخطاء تلقائياً.

- Express 4: لا يحدث ذلك؛ الخطأ يصبح "unhandled rejection" والطلب يعلق. الحل الشائع غلاف asyncHandler (في المثال الكامل) أو حزمة express-async-errors.

المعالج المركزي

function errorHandler(err, req, res, next) {
  const status = err.status || 500;
  if (status >= 500) console.error(err);          // سجّل كاملاً للمطور
  res.status(status).json({
    error: {
      code: err.code || "INTERNAL",
      message: status < 500 ? err.message : "حدث خطأ في الخادم",   // لا تكشف التفاصيل
      ...(process.env.NODE_ENV !== "production" && status >= 500 ? { stack: err.stack } : {}),
    },
  });
}

لا ترسل err.stack أو رسائل قاعدة البيانات للعميل في الإنتاج: تكشف بنية النظام وتساعد المهاجمين.

404 للمسارات غير الموجودة

Middleware عادي بعد كل المسارات وقبل معالج الأخطاء: app.use((req, res) => res.status(404).json(...)).

أخطاء خارج الطلبات

process.on("unhandledRejection") و process.on("uncaughtException"): سجّل الخطأ ثم أنهِ العملية بشكل منظم ودع مدير العمليات (PM2، Docker، المنصة) يعيد تشغيلها — الاستمرار بعد خطأ مجهول قد يترك التطبيق في حالة فاسدة.

تمرين

أضف للمثال الكامل خطأ ConflictError (409) يُرمى عند تكرار البريد، وتأكد أن الاستجابة في الإنتاج (NODE_ENV=production) لا تحتوي stack.

// ===== Custom Error Class =====
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
    this.isOperational = true;
    
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  constructor(resource = 'Resource') {
    super(`${resource} غير موجود`, 404);
  }
}

class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'غير مصرح') {
    super(message, 401);
  }
}

// ===== Async Handler =====
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// ===== Usage in Routes =====
app.get('/api/users/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    throw new NotFoundError('المستخدم');
  }
  res.json({ success: true, data: user });
}));

// ===== Global Error Handler =====
const errorHandler = (err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';
  
  // تسجيل الخطأ
  console.error('Error:', {
    message: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method
  });
  
  if (process.env.NODE_ENV === 'development') {
    // Development: إرجاع كل التفاصيل
    res.status(err.statusCode).json({
      success: false,
      error: {
        message: err.message,
        stack: err.stack,
        status: err.status
      }
    });
  } else {
    // Production: إخفاء التفاصيل
    if (err.isOperational) {
      res.status(err.statusCode).json({
        success: false,
        error: { message: err.message }
      });
    } else {
      // Programming error
      res.status(500).json({
        success: false,
        error: { message: 'حدث خطأ ما!' }
      });
    }
  }
};

// ===== 404 Handler =====
app.all('*', (req, res, next) => {
  next(new NotFoundError(`الـ Route ${req.originalUrl}`));
});

app.use(errorHandler);
اختبر فهمك