ไม่ใช่เรื่องยากที่จะเห็นว่าบางคนกำลังดิ้นรนเพื่อจัดการกับข้อผิดพลาดและบางคนก็พลาดไปเลย การจัดการข้อผิดพลาดอย่างถูกต้องไม่เพียง แต่ช่วยลดเวลาในการพัฒนาโดยการค้นหาจุดบกพร่องและข้อผิดพลาดได้ง่าย แต่ยังพัฒนาโค้ดเบสที่มีประสิทธิภาพสำหรับแอปพลิเคชันขนาดใหญ่
โดยเฉพาะอย่างยิ่ง, นักพัฒนา Node.js บางครั้งพบว่าตัวเองทำงานกับโค้ดที่ไม่สะอาดในขณะที่จัดการข้อผิดพลาดประเภทต่างๆโดยใช้ตรรกะเดียวกันทุกที่เพื่อจัดการกับข้อผิดพลาด พวกเขาเอาแต่ถามตัวเอง “ Node.js จัดการข้อผิดพลาดไม่ดีหรือไม่” หรือ ถ้าไม่มีจะจัดการอย่างไร” คำตอบของฉันสำหรับพวกเขาคือ “ ไม่ Node.js ก็ไม่เลวเลย ขึ้นอยู่กับนักพัฒนาของเรา”
นี่คือหนึ่งในวิธีแก้ปัญหาที่ฉันชอบ
ก่อนอื่นจำเป็นต้องมีความเข้าใจอย่างชัดเจนเกี่ยวกับข้อผิดพลาดใน Node.js โดยทั่วไปข้อผิดพลาด Node.js จะแบ่งออกเป็นสองประเภท: ข้อผิดพลาดในการปฏิบัติงาน และ ข้อผิดพลาดของโปรแกรมเมอร์ .
ด้วยเหตุนี้คุณจึงไม่มีปัญหาในการแยกแยะข้อผิดพลาดทั้งสองประเภทนี้: ข้อผิดพลาดในการดำเนินการเป็นส่วนหนึ่งของแอปพลิเคชันและข้อผิดพลาดของโปรแกรมเมอร์เป็นข้อบกพร่องที่เกิดจากนักพัฒนา คำถามเชิงตรรกะที่ตามมาคือ: “ เหตุใดจึงมีประโยชน์ที่จะแบ่งออกเป็นสองประเภทและจัดการกับพวกเขา?”
หากไม่มีความเข้าใจอย่างชัดเจนเกี่ยวกับข้อผิดพลาดคุณอาจรู้สึกอยากรีสตาร์ทแอปพลิเคชันทุกครั้งที่เกิดข้อผิดพลาด ควรรีสตาร์ทแอปพลิเคชันเนื่องจากข้อผิดพลาด“ ไม่พบไฟล์” เมื่อผู้ใช้หลายพันคนเพลิดเพลินกับแอปพลิเคชันหรือไม่ ไม่ได้อย่างแน่นอน.
แต่ข้อผิดพลาดของโปรแกรมเมอร์ล่ะ? มันสมเหตุสมผลหรือไม่ที่จะให้แอปพลิเคชันทำงานต่อไปเมื่อมีข้อผิดพลาดที่ไม่รู้จักปรากฏขึ้นซึ่งอาจส่งผลให้เกิดสโนว์บอลที่ไม่คาดคิดในแอปพลิเคชัน อีกครั้งไม่แน่นอน!
สมมติว่าคุณมีประสบการณ์กับ async JavaScript และ Node.js มาบ้างคุณอาจเคยพบข้อเสียเมื่อใช้การโทรกลับเพื่อจัดการกับข้อผิดพลาด พวกเขาบังคับให้คุณตรวจสอบข้อผิดพลาดไปจนถึงข้อผิดพลาดที่ซ้อนกันทำให้เกิดปัญหา 'การเรียกกลับ' ที่ฉาวโฉ่ซึ่งทำให้ยากที่จะทำตามขั้นตอนของโค้ด
การใช้คำสัญญาหรือ async / await เป็นการทดแทนการโทรกลับที่ดี โฟลว์โค้ดทั่วไปของ async / await มีลักษณะดังนี้:
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 และคำอธิบายโดยการขยายคลาส Error จะทำให้มีข้อมูลมากขึ้น
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 ของเรา: องค์ประกอบการจัดการข้อผิดพลาดจากส่วนกลาง
โดยปกติแล้วเป็นความคิดที่ดีที่จะสร้างส่วนประกอบการจัดการข้อผิดพลาดจากส่วนกลางเพื่อหลีกเลี่ยงการซ้ำซ้อนของรหัสที่อาจเกิดขึ้นเมื่อจัดการข้อผิดพลาด องค์ประกอบการจัดการข้อผิดพลาดมีหน้าที่ในการทำให้ข้อผิดพลาดที่จับได้นั้นเข้าใจได้โดยตัวอย่างเช่นการส่งการแจ้งเตือนไปยังผู้ดูแลระบบ (หากจำเป็น) การถ่ายโอนเหตุการณ์ไปยังบริการตรวจสอบเช่น 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 { 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 ที่กำหนดเอง:
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();
สิ่งที่ให้โดยพื้นฐานคือการบันทึกในหลายระดับที่แตกต่างกันในรูปแบบที่มีสีที่ชัดเจนและเข้าสู่สื่อเอาต์พุตที่แตกต่างกันตามสภาพแวดล้อมรันไทม์ ข้อดีคือคุณสามารถดูและสืบค้นบันทึกโดยใช้ API ในตัวของ 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) => { throw reason; }); process.on('uncaughtException', (error: Error) => { errorHandler.handleError(error); if (!errorHandler.isTrustedError(error)) { process.exit(1); } });
เมื่อพูดและทำเสร็จแล้วคุณควรตระหนักว่าการจัดการข้อผิดพลาดไม่ใช่ทางเลือกพิเศษ แต่เป็นส่วนสำคัญของแอปพลิเคชันทั้งในขั้นตอนการพัฒนาและในการผลิต
กลยุทธ์ในการจัดการข้อผิดพลาดในองค์ประกอบเดียวใน Node.js จะช่วยให้นักพัฒนาประหยัดเวลาอันมีค่าและเขียนโค้ดที่สะอาดและดูแลรักษาได้โดยหลีกเลี่ยงการทำซ้ำโค้ดและบริบทข้อผิดพลาดที่ขาดหายไป
ฉันหวังว่าคุณจะสนุกกับการอ่านบทความนี้และพบว่าเวิร์กโฟลว์การจัดการข้อผิดพลาดและการใช้งานที่กล่าวถึงเป็นประโยชน์สำหรับการสร้างไฟล์ codebase ที่แข็งแกร่งใน Node.js .
การจัดการข้อผิดพลาดที่เหมาะสมทำให้แอปมีประสิทธิภาพส่งผลให้ผู้ใช้มีประสบการณ์ที่เหนือกว่าและเพิ่มประสิทธิภาพ
หลักการออกแบบนี้ทำให้ส่วนหนึ่งของงานมีความโดดเด่นเหนือส่วนอื่นๆ
ใช้คำสัญญาหรือ async / await จัดการข้อผิดพลาดในองค์ประกอบที่รวมศูนย์จัดการข้อยกเว้นที่ไม่ถูกจับ
การจัดการข้อผิดพลาดเป็นส่วนหนึ่งของแอปทั้งหมดที่ต้องมี ป้องกันไม่ให้แอปเกิดข้อผิดพลาดและช่วยประหยัดเวลาในการพัฒนาอันมีค่า
โดยสมัครสมาชิก process.on ('unhandledRejection'), process.on ('uncaughtException')
ไม่มันไม่ใช่. ขึ้นอยู่กับนักพัฒนาและวิธีที่พวกเขาเลือกที่จะจัดการกับพวกเขา