معالجة الأخطاء في Express
المعالجة الصحيحة للأخطاء أمر أساسي لبناء واجهات برمجية قوية وسهلة الاستخدام. يوفر Express آلية قوية لمعالجة الأخطاء تسمح لك بالتقاط الأخطاء ومعالجتها والاستجابة لها بلطف، مما يحسن تجربة المستخدم وقابلية صيانة التطبيق.
أنواع الأخطاء
- الأخطاء المتزامنة: التي يتم طرحها في معالجات المسار والتقاطها تلقائيًا
- الأخطاء غير المتزامنة: أخطاء في الوعود التي تحتاج إلى معالجة صريحة
- أخطاء التحقق: مدخلات المستخدم غير الصالحة (حالة 422)
- أخطاء المصادقة: الوصول غير المصرح به (حالة 401/403)
- أخطاء عدم الوجود: المورد غير موجود (حالة 404)
- أخطاء الخادم: مشاكل خادم غير متوقعة (حالة 500)
المعالجة الأساسية للأخطاء
يلتقط Express الأخطاء المتزامنة تلقائيًا:
const express = require('express');
const app = express();
// خطأ متزامن - يتم التقاطه تلقائيًا
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
// طرح خطأ - Express يلتقطه
throw new Error('المستخدم غير موجود');
}
res.json(user);
});
// معالج الأخطاء الافتراضي (يجب تعريفه أخيرًا)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
message: 'حدث خطأ ما!'
});
});
app.listen(3000);
ملاحظة: يجب أن يحتوي middleware معالجة الأخطاء على أربعة معاملات (err, req, res, next) ويجب تعريفه بعد جميع middleware والمسارات الأخرى.
معالجة الأخطاء غير المتزامنة
تحتاج الأخطاء غير المتزامنة إلى تمريرها صراحة إلى next():
// بدون async/await - استخدم .catch()
app.get('/users/:id', (req, res, next) => {
User.findById(req.params.id)
.then(user => {
if (!user) {
throw new Error('المستخدم غير موجود');
}
res.json(user);
})
.catch(next); // تمرير الخطأ إلى معالج الأخطاء
});
// مع async/await - لف في try-catch
app.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
throw new Error('المستخدم غير موجود');
}
res.json(user);
} catch (error) {
next(error); // تمرير الخطأ إلى معالج الأخطاء
}
});
فئات الأخطاء المخصصة
أنشئ فئات أخطاء مخصصة لأنواع الأخطاء المختلفة:
// errors/AppError.js
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);
}
}
module.exports = AppError;
// errors/NotFoundError.js
const AppError = require('./AppError');
class NotFoundError extends AppError {
constructor(resource = 'المورد') {
super(`${resource} غير موجود`, 404);
}
}
module.exports = NotFoundError;
// errors/ValidationError.js
const AppError = require('./AppError');
class ValidationError extends AppError {
constructor(errors) {
super('فشل التحقق', 422);
this.errors = errors;
}
}
module.exports = ValidationError;
// errors/UnauthorizedError.js
const AppError = require('./AppError');
class UnauthorizedError extends AppError {
constructor(message = 'وصول غير مصرح به') {
super(message, 401);
}
}
module.exports = UnauthorizedError;
استخدام فئات الأخطاء المخصصة
const NotFoundError = require('./errors/NotFoundError');
const ValidationError = require('./errors/ValidationError');
const UnauthorizedError = require('./errors/UnauthorizedError');
// معالجات المسار
app.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
throw new NotFoundError('المستخدم');
}
res.json(user);
} catch (error) {
next(error);
}
});
app.post('/users', async (req, res, next) => {
try {
// التحقق
const errors = validateUser(req.body);
if (errors.length > 0) {
throw new ValidationError(errors);
}
const user = await User.create(req.body);
res.status(201).json(user);
} catch (error) {
next(error);
}
});
app.get('/admin', checkAuth, (req, res, next) => {
if (!req.user.isAdmin) {
return next(new UnauthorizedError('مطلوب وصول المسؤول'));
}
res.json({ message: 'لوحة المسؤول' });
});
معالج الأخطاء المركزي
أنشئ middleware شامل لمعالجة الأخطاء:
// middleware/errorHandler.js
const AppError = require('../errors/AppError');
const errorHandler = (err, req, res, next) => {
// تعيين القيم الافتراضية
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
// استجابات الأخطاء في التطوير مقابل الإنتاج
if (process.env.NODE_ENV === 'development') {
sendErrorDev(err, res);
} else {
sendErrorProd(err, res);
}
};
const sendErrorDev = (err, res) => {
// إرسال خطأ مفصل في التطوير
res.status(err.statusCode).json({
success: false,
status: err.status,
message: err.message,
error: err,
stack: err.stack,
errors: err.errors || undefined
});
};
const sendErrorProd = (err, res) => {
// خطأ تشغيلي موثوق: إرسال رسالة للعميل
if (err.isOperational) {
res.status(err.statusCode).json({
success: false,
status: err.status,
message: err.message,
errors: err.errors || undefined
});
}
// خطأ برمجي أو غير معروف: لا تكشف التفاصيل
else {
// تسجيل الخطأ للتصحيح
console.error('خطأ:', err);
// إرسال رسالة عامة
res.status(500).json({
success: false,
status: 'error',
message: 'حدث خطأ ما'
});
}
};
module.exports = errorHandler;
// استخدم في app.js
const errorHandler = require('./middleware/errorHandler');
// ... المسارات ...
// معالج الأخطاء (يجب أن يكون أخيرًا)
app.use(errorHandler);
أفضل ممارسة: لا تكشف أبدًا عن تفاصيل الأخطاء الحساسة (تتبع المكدس، أخطاء قاعدة البيانات) في الإنتاج. سجلها على جانب الخادم بدلاً من ذلك.
غلاف معالج الأخطاء غير المتزامن
أنشئ غلافًا لتجنب كتل try-catch في كل مسار غير متزامن:
// utils/asyncHandler.js
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
module.exports = asyncHandler;
// الاستخدام - لا حاجة لمزيد من try-catch!
const asyncHandler = require('./utils/asyncHandler');
const NotFoundError = require('./errors/NotFoundError');
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new NotFoundError('المستخدم');
}
res.json(user);
}));
app.post('/users', asyncHandler(async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
}));
// يتم التقاط الأخطاء تلقائيًا وتمريرها إلى معالج الأخطاء!
معالجة أنواع الأخطاء المحددة
تعامل مع أنواع الأخطاء المختلفة بشكل مناسب:
const errorHandler = (err, req, res, next) => {
// MongoDB CastError (تنسيق معرف غير صالح)
if (err.name === 'CastError') {
err = new AppError(`${err.path} غير صالح: ${err.value}`, 400);
}
// خطأ مفتاح مكرر في MongoDB
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
err = new AppError(`قيمة مكررة لـ ${field}`, 409);
}
// خطأ التحقق في MongoDB
if (err.name === 'ValidationError') {
const errors = Object.values(err.errors).map(e => e.message);
err = new ValidationError(errors);
}
// أخطاء JWT
if (err.name === 'JsonWebTokenError') {
err = new UnauthorizedError('رمز غير صالح');
}
if (err.name === 'TokenExpiredError') {
err = new UnauthorizedError('انتهت صلاحية الرمز');
}
// أخطاء تحميل ملف Multer
if (err.name === 'MulterError') {
if (err.code === 'LIMIT_FILE_SIZE') {
err = new AppError('حجم الملف كبير جدًا', 413);
}
}
// إرسال استجابة الخطأ
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
if (process.env.NODE_ENV === 'development') {
sendErrorDev(err, res);
} else {
sendErrorProd(err, res);
}
};
معالج 404 غير موجود
تعامل مع المسارات غير الموجودة:
const NotFoundError = require('./errors/NotFoundError');
// ... جميع المسارات ...
// معالج 404 - يجب أن يكون بعد جميع المسارات
app.all('*', (req, res, next) => {
next(new NotFoundError(`المسار ${req.originalUrl} غير موجود`));
});
// معالج الأخطاء - يجب أن يكون أخيرًا
app.use(errorHandler);
تسجيل الأخطاء
سجل الأخطاء للتصحيح والمراقبة:
const winston = require('winston');
// تكوين المسجل
const logger = winston.createLogger({
level: 'error',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// إضافة نقل Console في التطوير
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// معالج الأخطاء مع التسجيل
const errorHandler = (err, req, res, next) => {
// تسجيل الخطأ
logger.error({
message: err.message,
stack: err.stack,
statusCode: err.statusCode,
timestamp: new Date().toISOString(),
method: req.method,
url: req.originalUrl,
ip: req.ip,
userId: req.user?.id
});
// إرسال الاستجابة
err.statusCode = err.statusCode || 500;
if (process.env.NODE_ENV === 'development') {
sendErrorDev(err, res);
} else {
sendErrorProd(err, res);
}
};
تحذير: لا تسجل أبدًا معلومات حساسة مثل كلمات المرور أو الرموز أو البيانات الشخصية في سجلات الأخطاء. طهر البيانات قبل التسجيل.
معالجة الرفضات غير المعالجة
التقط رفضات الوعود غير المعالجة عالميًا:
// app.js
const app = require('./app');
const server = app.listen(3000, () => {
console.log('الخادم يعمل على المنفذ 3000');
});
// معالجة رفضات الوعود غير المعالجة
process.on('unhandledRejection', (err) => {
console.error('رفض غير معالج! إيقاف التشغيل...');
console.error(err.name, err.message);
// إغلاق الخادم بلطف
server.close(() => {
process.exit(1);
});
});
// معالجة الاستثناءات غير الملتقطة
process.on('uncaughtException', (err) => {
console.error('استثناء غير ملتقط! إيقاف التشغيل...');
console.error(err.name, err.message);
process.exit(1);
});
تنسيق استجابة الخطأ
تنسيق استجابة خطأ متسق:
// تنسيق استجابة خطأ الإنتاج
{
"success": false,
"status": "fail",
"message": "المستخدم غير موجود",
"errors": [
{
"field": "email",
"message": "تنسيق البريد الإلكتروني غير صالح"
}
]
}
// تنسيق استجابة خطأ التطوير (يتضمن معلومات التصحيح)
{
"success": false,
"status": "error",
"message": "فشل اتصال قاعدة البيانات",
"error": {
"name": "MongoNetworkError",
"message": "انتهاء مهلة الاتصال"
},
"stack": "Error: Connection timeout\n at ..."
}
تمرين: نفذ معالجة شاملة للأخطاء لواجهة برمجية للمنتجات:
- أنشئ فئات أخطاء مخصصة: NotFoundError, ValidationError, UnauthorizedError, ConflictError
- نفذ غلاف asyncHandler للتخلص من كتل try-catch
- أنشئ middleware معالج أخطاء مركزي مع أوضاع dev/prod
- تعامل مع أخطاء MongoDB (CastError، مفتاح مكرر، تحقق)
- نفذ معالج 404 للمسارات غير الموجودة
- أضف تسجيل الأخطاء باستخدام Winston
- اختبر معالجة الأخطاء بتشغيل سيناريوهات أخطاء مختلفة
معايير استجابة أخطاء الواجهة البرمجية
اتبع معايير الصناعة لاستجابات أخطاء الواجهة البرمجية:
- أكواد حالة HTTP: استخدم أكواد الحالة المناسبة (400، 401، 403، 404، 422، 500)
- رسائل الخطأ: رسائل واضحة وقابلة للتنفيذ للمستخدمين
- أكواد الأخطاء: أكواد أخطاء مخصصة اختيارية للمعالجة من جانب العميل
- أخطاء مستوى الحقل: للتحقق، حدد الحقول التي بها أخطاء
- تنسيق متسق: نفس البنية عبر جميع نقاط النهاية
- لا بيانات حساسة: لا تكشف أبدًا عن كلمات المرور أو الرموز أو التفاصيل الداخلية
- التوثيق: وثق جميع استجابات الأخطاء المحتملة
// مثال على استجابة خطأ شاملة
{
"success": false,
"status": "fail",
"message": "فشل التحقق",
"errorCode": "VALIDATION_ERROR",
"errors": [
{
"field": "email",
"message": "تنسيق البريد الإلكتروني غير صالح",
"code": "INVALID_EMAIL"
},
{
"field": "password",
"message": "كلمة المرور يجب أن تكون 8 أحرف على الأقل",
"code": "PASSWORD_TOO_SHORT"
}
],
"timestamp": "2024-01-15T10:30:00.000Z",
"path": "/api/users"
}
أفضل ممارسات معالجة الأخطاء
- استخدم دائمًا middleware معالجة الأخطاء، لا تعالج الأخطاء في المسارات
- أنشئ فئات أخطاء مخصصة لأنواع الأخطاء المختلفة
- استخدم غلاف asyncHandler لتبسيط معالجة الأخطاء غير المتزامنة
- ميز بين الأخطاء التشغيلية والبرمجية
- لا تكشف أبدًا عن معلومات حساسة في رسائل الخطأ
- سجل جميع الأخطاء مع السياق (الطابع الزمني، المستخدم، تفاصيل الطلب)
- أرجع تفاصيل خطأ مختلفة في التطوير مقابل الإنتاج
- استخدم أكواد حالة HTTP المناسبة بشكل متسق
- قدم رسائل خطأ قابلة للتنفيذ تساعد المستخدمين على إصلاح المشكلات
- تعامل مع الرفضات غير المعالجة والاستثناءات غير الملتقطة
- نفذ إيقاف تشغيل لطيف عند الأخطاء الحرجة
- وثق جميع استجابات الأخطاء المحتملة في توثيق الواجهة البرمجية
في الدرس التالي، سنتعلم كيفية تنفيذ المصادقة باستخدام JSON Web Tokens (JWT) لتأمين نقاط نهاية واجهتنا البرمجية وإدارة جلسات المستخدم.