كيفية بناء نظام معالجة الأخطاء Node.js
نشرت: 2022-03-11ليس من الصعب أن نرى أن بعض الناس يكافحون للتعامل مع الأخطاء ، والبعض الآخر يفتقدها تمامًا. لا يعني التعامل مع الأخطاء بشكل صحيح تقليل وقت التطوير من خلال العثور على الأخطاء والأخطاء بسهولة فحسب ، بل يعني أيضًا تطوير قاعدة بيانات قوية للتطبيقات واسعة النطاق.
على وجه الخصوص ، يجد مطورو Node.js أنفسهم أحيانًا يعملون برمز غير نظيف أثناء التعامل مع أنواع مختلفة من الأخطاء ، ويطبقون نفس المنطق بشكل غير صحيح في كل مكان للتعامل معها. إنهم يسألون أنفسهم باستمرار "هل Node.js سيء في معالجة الأخطاء؟" أو إذا لم يكن كذلك ، فكيف نتعامل معها؟ " جوابي لهم هو "لا ، Node.js ليس سيئًا على الإطلاق. هذا يعتمد علينا نحن المطورين. "
هنا أحد الحلول المفضلة لدي لذلك.
أنواع الأخطاء في Node.js
بادئ ذي بدء ، من الضروري أن يكون لديك فهم واضح للأخطاء في Node.js. بشكل عام ، تنقسم أخطاء Node.js إلى فئتين متميزتين: الأخطاء التشغيلية وأخطاء المبرمج .
- تمثل أخطاء التشغيل مشاكل وقت التشغيل التي من المتوقع أن يتم التعامل معها بطريقة مناسبة. لا تعني الأخطاء التشغيلية أن التطبيق نفسه به أخطاء ، ولكن يحتاج المطورون إلى التعامل معها بعناية. تتضمن أمثلة الأخطاء التشغيلية "نفاد الذاكرة" و "إدخال غير صالح لنقطة نهاية API" وما إلى ذلك.
- تمثل أخطاء المبرمج أخطاء غير متوقعة في التعليمات البرمجية سيئة الكتابة. يقصدون أن الشفرة نفسها بها بعض المشكلات التي يجب حلها وتم ترميزها بشكل خاطئ. خير مثال على ذلك هو محاولة قراءة خاصية "غير محدد". لإصلاح المشكلة ، يجب تغيير الرمز. هذا خطأ قام به المطور ، وليس خطأ تشغيليًا.
مع وضع ذلك في الاعتبار ، يجب ألا تواجه أي مشكلة في التمييز بين هاتين الفئتين من الأخطاء: الأخطاء التشغيلية جزء طبيعي من التطبيق ، وأخطاء المبرمج هي أخطاء يتسبب فيها المطورون. السؤال المنطقي التالي هو: "لماذا من المفيد تقسيمهم إلى فئتين والتعامل معهم؟"
بدون فهم واضح للأخطاء ، قد تشعر برغبة في إعادة تشغيل تطبيق كلما حدث خطأ. هل يعقل إعادة تشغيل أحد التطبيقات بسبب أخطاء "لم يتم العثور على الملف" عندما يستمتع آلاف المستخدمين بالتطبيق؟ بالطبع لا.
لكن ماذا عن أخطاء المبرمج؟ هل من المنطقي الاستمرار في تشغيل التطبيق عند ظهور خطأ غير معروف قد ينتج عنه تأثير كرة ثلج غير متوقع في التطبيق؟ مرة أخرى ، بالتأكيد لا!
حان الوقت للتعامل مع الأخطاء بشكل صحيح
بافتراض أن لديك بعض الخبرة مع JavaScript و Node.js غير المتزامن ، فربما تكون قد واجهت بعض العيوب عند استخدام عمليات الاسترجاعات للتعامل مع الأخطاء. إنها تجبرك على التحقق من الأخطاء وصولاً إلى الأخطاء المتداخلة ، مما يتسبب في مشكلات "رد الاتصال" سيئة السمعة التي تجعل من الصعب متابعة تدفق التعليمات البرمجية.
يعد استخدام الوعود أو غير المتزامن / انتظار بديلاً جيدًا لعمليات الاسترجاعات. يبدو التدفق النموذجي للشفرة غير المتزامن / انتظار كما يلي:
const doAsyncJobs = async () => { try { const result1 = await job1(); const result2 = await job2(result1); const result3 = await job3(result2); return await job4(result3); } catch (error) { console.error(error); } finally { await anywayDoThisJob(); } }
يعد استخدام كائن الخطأ المدمج في Node.js ممارسة جيدة لأنه يتضمن معلومات بديهية وواضحة حول أخطاء مثل StackTrace ، والتي يعتمد عليها معظم المطورين لتتبع جذر الخطأ. كما أن الخصائص الإضافية المفيدة مثل رمز حالة HTTP والوصف من خلال توسيع فئة الخطأ ستجعلها أكثر إفادة.
class BaseError extends Error { public readonly name: string; public readonly httpCode: HttpStatusCode; public readonly isOperational: boolean; constructor(name: string, httpCode: HttpStatusCode, description: string, isOperational: boolean) { super(description); Object.setPrototypeOf(this, new.target.prototype); this.name = name; this.httpCode = httpCode; this.isOperational = isOperational; Error.captureStackTrace(this); } } //free to extend the BaseError class APIError extends BaseError { constructor(name, httpCode = HttpStatusCode.INTERNAL_SERVER, isOperational = true, description = 'internal server error') { super(name, httpCode, isOperational, description); } }
لقد قمت فقط بتنفيذ بعض رموز حالة HTTP من أجل البساطة ، لكن يمكنك إضافة المزيد لاحقًا.
export enum HttpStatusCode { OK = 200, BAD_REQUEST = 400, NOT_FOUND = 404, INTERNAL_SERVER = 500, }
ليست هناك حاجة لتمديد BaseError أو APIError ، ولكن لا بأس من تمديدها للأخطاء الشائعة وفقًا لاحتياجاتك وتفضيلاتك الشخصية.
class HTTP400Error extends BaseError { constructor(description = 'bad request') { super('NOT FOUND', HttpStatusCode.BAD_REQUEST, true, description); } }
إذا كيف يمكنك أن تستفيد منها؟ فقط رمي هذا في:
... const user = await User.getUserById(1); if (user === null) throw new APIError( 'NOT FOUND', HttpStatusCode.NOT_FOUND, true, 'detailed explanation' );
معالجة أخطاء Node.js المركزية
الآن ، نحن جاهزون لبناء المكون الرئيسي لنظام معالجة الأخطاء Node.js لدينا: مكون معالجة الأخطاء المركزي.
عادة ما تكون فكرة جيدة لبناء مكون مركزي لمعالجة الأخطاء من أجل تجنب تكرار الكود المحتمل عند معالجة الأخطاء. يعد مكون معالجة الأخطاء مسؤولاً عن جعل الأخطاء التي تم اكتشافها مفهومة ، على سبيل المثال ، إرسال إشعارات إلى مسؤولي النظام (إذا لزم الأمر) ، ونقل الأحداث إلى خدمة مراقبة مثل Sentry.io ، وتسجيلها.
فيما يلي سير عمل أساسي للتعامل مع الأخطاء:
في بعض أجزاء الكود ، يتم اكتشاف الأخطاء لنقلها إلى برمجيات وسيطة لمعالجة الأخطاء.
... try { userService.addNewUser(req.body).then((newUser: User) => { res.status(200).json(newUser); }).catch((error: Error) => { next(error) }); } catch (error) { next(error); } ...
تعد البرامج الوسيطة لمعالجة الأخطاء مكانًا جيدًا للتمييز بين أنواع الأخطاء وإرسالها إلى مكون معالجة الأخطاء المركزي. من المؤكد أن معرفة أساسيات معالجة الأخطاء في برمجية Express.js الوسيطة.

app.use(async (err: Error, req: Request, res: Response, next: NextFunction) => { if (!errorHandler.isTrustedError(err)) { next(err); } await errorHandler.handleError(err); });
الآن ، يمكن للمرء أن يتخيل الشكل الذي يجب أن يبدو عليه المكون المركزي لأننا استخدمنا بالفعل بعض وظائفه. ضع في اعتبارك أن الأمر متروك لك تمامًا في كيفية تنفيذه ، ولكن قد يبدو الأمر كما يلي:
class ErrorHandler { public async handleError(err: Error): Promise<void> { await logger.error( 'Error message from the centralized error-handling component', err, ); await sendMailToAdminIfCritical(); await sendEventsToSentry(); } public isTrustedError(error: Error) { if (error instanceof BaseError) { return error.isOperational; } return false; } } export const errorHandler = new ErrorHandler();
في بعض الأحيان ، يؤدي إخراج "console.log" الافتراضي إلى صعوبة تتبع الأخطاء. بدلاً من ذلك ، قد يكون من الأفضل طباعة الأخطاء بطريقة منسقة بحيث يمكن للمطورين فهم المشكلات بسرعة والتأكد من إصلاحها.
بشكل عام ، سيوفر هذا الوقت للمطورين مما يسهل تتبع الأخطاء ومعالجتها من خلال زيادة ظهورها. إنه لقرار جيد أن تستخدم أداة تسجيل قابلة للتخصيص مثل winston أو morgan.
هنا هو مسجل وينستون مخصص:
const customLevels = { levels: { trace: 5, debug: 4, info: 3, warn: 2, error: 1, fatal: 0, }, colors: { trace: 'white', debug: 'green', info: 'green', warn: 'yellow', error: 'red', fatal: 'red', }, }; const formatter = winston.format.combine( winston.format.colorize(), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.splat(), winston.format.printf((info) => { const { timestamp, level, message, ...meta } = info; return `${timestamp} [${level}]: ${message} ${ Object.keys(meta).length ? JSON.stringify(meta, null, 2) : '' }`; }), ); class Logger { private logger: winston.Logger; constructor() { const prodTransport = new winston.transports.File({ filename: 'logs/error.log', level: 'error', }); const transport = new winston.transports.Console({ format: formatter, }); this.logger = winston.createLogger({ level: isDevEnvironment() ? 'trace' : 'error', levels: customLevels.levels, transports: [isDevEnvironment() ? transport : prodTransport], }); winston.addColors(customLevels.colors); } trace(msg: any, meta?: any) { this.logger.log('trace', msg, meta); } debug(msg: any, meta?: any) { this.logger.debug(msg, meta); } info(msg: any, meta?: any) { this.logger.info(msg, meta); } warn(msg: any, meta?: any) { this.logger.warn(msg, meta); } error(msg: any, meta?: any) { this.logger.error(msg, meta); } fatal(msg: any, meta?: any) { this.logger.log('fatal', msg, meta); } } export const logger = new Logger();
ما يوفره بشكل أساسي هو التسجيل في عدة مستويات مختلفة بطريقة منسقة ، بألوان واضحة ، وتسجيل الدخول إلى وسائط إخراج مختلفة وفقًا لبيئة وقت التشغيل. الشيء الجيد في هذا هو أنه يمكنك مشاهدة السجلات والاستعلام عنها باستخدام واجهات برمجة التطبيقات المضمنة في Winston. علاوة على ذلك ، يمكنك استخدام أداة تحليل السجل لتحليل ملفات السجل المنسقة للحصول على مزيد من المعلومات المفيدة حول التطبيق. إنه رائع ، أليس كذلك؟
حتى هذه النقطة ، ناقشنا في الغالب التعامل مع الأخطاء التشغيلية. ماذا عن أخطاء المبرمج؟ أفضل طريقة للتعامل مع هذه الأخطاء هي التعطل فورًا وإعادة التشغيل بأمان مع إعادة تشغيل تلقائية مثل PM2 - والسبب هو أن أخطاء المبرمج غير متوقعة ، لأنها أخطاء فعلية قد تؤدي إلى إنهاء التطبيق في حالة خاطئة والتصرف بطريقة غير متوقعة.
process.on('uncaughtException', (error: Error) => { errorHandler.handleError(error); if (!errorHandler.isTrustedError(error)) { process.exit(1); } });
أخيرًا وليس آخرًا ، سوف أذكر التعامل مع الاستثناءات ورفض الوعود غير المنفذة.
قد تجد نفسك تقضي الكثير من الوقت في التعامل مع الوعود عند العمل على تطبيقات Node.js / Express. ليس من الصعب رؤية رسائل تحذير بشأن رفض الوعود غير المنفذة عندما تنسى التعامل مع حالات الرفض.
رسائل التحذير لا تفعل الكثير باستثناء التسجيل ، ولكن من الممارسات الجيدة استخدام احتياطي لائق والاشتراك في process.on('unhandledRejection', callback)
.
قد يبدو تدفق معالجة الأخطاء النموذجي كما يلي:
// somewhere in the code ... User.getUserById(1).then((firstUser) => { if (firstUser.isSleeping === false) throw new Error('He is not sleeping!'); }); ... // get the unhandled rejection and throw it to another fallback handler we already have. process.on('unhandledRejection', (reason: Error, promise: Promise<any>) => { throw reason; }); process.on('uncaughtException', (error: Error) => { errorHandler.handleError(error); if (!errorHandler.isTrustedError(error)) { process.exit(1); } });
تغليف
عندما يتم قول وفعل كل شيء ، يجب أن تدرك أن معالجة الأخطاء ليست إضافية اختيارية ولكنها جزء أساسي من التطبيق ، سواء في مرحلة التطوير أو في الإنتاج.
ستضمن إستراتيجية معالجة الأخطاء في مكون واحد في Node.js للمطورين توفير وقت ثمين وكتابة تعليمات برمجية نظيفة وقابلة للصيانة من خلال تجنب تكرار الكود وفقدان سياق الخطأ.
أتمنى أن تكون قد استمتعت بقراءة هذا المقال ووجدت أن سير العمل والتنفيذ الذي تمت مناقشته لمعالجة الأخطاء مفيد لبناء قاعدة رموز قوية في Node.js.
مزيد من القراءة على مدونة Toptal Engineering:
- استخدام مسارات Express.js لمعالجة الأخطاء المستندة إلى الوعد