الخطوات
-
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
كتابة 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
تسجيل المعالج آخراً في app.js
لن يستدعي Express الـ middleware للأخطاء إلا إذا مرّر middleware أو مسار سابق خطأً إلى
next(err)، أو رمى خطأً بشكل متزامن. سجّله كآخر استدعاء لـapp.use.javascriptconst 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
تغليف معالجات المسارات غير المتزامنة
لا يعترض 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
الترقية إلى Express 5 للدعم الأصلي للـ async
يعترض Express 5 (صدر عام 2024) الـ Promises المرفوضة من معالجات المسارات تلقائياً، فلا حاجة لـ
asyncHandlerبعد الآن. إن كنت تبدأ مشروعاً جديداً، استخدم Express 5.javascriptnpm 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
التمييز بين الأخطاء التشغيلية وأخطاء المبرمج
هذا التمييز مهم لعمليات الإنجاز. الخطأ التشغيلي (مستخدم غير موجود، فشل التحقق) متوقع وآمن للعرض للعميل. خطأ المبرمج (لا يمكن قراءة خاصية من undefined) يُشير إلى خلل — يجب أن يُطلق تنبيهاً، ويُرسل stack trace كاملاً إلى نظام التسجيل، بينما تكون الاستجابة للعميل 500 عاماً دون تفاصيل.
javascript// في المعالج المركزي: if (!isOperational) { logger.error({ err }, 'خطأ برمجي — تنبيه المناوب'); // في بعض الإعدادات يُفضَّل إعادة تشغيل العملية: // process.exit(1); // سيُعيد تشغيلها مدير العمليات (PM2 أو systemd) } -
7
إعادة شكل موحد لاستجابات الأخطاء
يجب أن تبدو كل استجابة خطأ من واجهة API بنفس الشكل. لا ينبغي للعملاء التخمين إن كان الخطأ في
errأوerrorأوmessageأوerrors[0]. اتفق على شكل واحد وطبّقه حصراً من خلال المعالج المركزي.json// كل استجابة خطأ من واجهة API: { "error": { "code": "NOT_FOUND", "message": "المستخدم غير موجود" } } // أخطاء التحقق يمكن توسيع الشكل: { "error": { "code": "VALIDATION_ERROR", "message": "فشل التحقق من الطلب", "fields": { "email": "يجب أن يكون عنوان بريد إلكتروني صحيح", "age": "يجب أن يكون عدداً صحيحاً موجباً" } } } -
8
لا تُسرّب Stack Trace في الإنتاج أبداً
تكشف Stack Traces عن بنية ملفاتك وإصدارات المكتبات وأحياناً أسماء المتغيرات — وكلها مفيدة للمهاجمين. يجب أن يتضمن المعالج المركزي Stack Trace في الاستجابة فقط عندما تكون البيئة مضبوطة صراحةً على development.
javascriptfunction 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 بأكمله. أضف تسجيلاً منظّماً وشكلاً موحداً للاستجابات، وسيصبح تطبيقك أسهل بكثير في التصحيح والمراقبة والصيانة.