معالجة الأخطاء 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);اختبر فهمك