วิธีสร้างระบบจัดการข้อผิดพลาด Node.js
เผยแพร่แล้ว: 2022-03-11ไม่ใช่เรื่องยากที่จะเห็นบางคนกำลังพยายามจัดการกับข้อผิดพลาด และบางคนถึงกับพลาดโดยสิ้นเชิง การจัดการข้อผิดพลาดอย่างถูกต้องไม่เพียงหมายถึงการลดเวลาในการพัฒนาโดยการค้นหาจุดบกพร่องและข้อผิดพลาดอย่างง่ายดายเท่านั้น แต่ยังรวมถึงการพัฒนาฐานโค้ดที่มีประสิทธิภาพสำหรับแอปพลิเคชันขนาดใหญ่ด้วย
โดยเฉพาะอย่างยิ่ง นักพัฒนา Node.js บางครั้งพบว่าตัวเองทำงานกับโค้ดที่ไม่สะอาดในขณะที่จัดการกับข้อผิดพลาดประเภทต่างๆ โดยใช้ตรรกะเดียวกันทุกที่อย่างไม่ถูกต้องเพื่อจัดการกับข้อผิดพลาด พวกเขาเอาแต่ถามตัวเองอยู่เสมอว่า "Node.js จัดการกับข้อผิดพลาดได้ไม่ดีหรือเปล่า" หรือ ถ้าไม่ใช่จะรับมืออย่างไร” คำตอบของฉันคือ “ไม่ Node.js ไม่ได้แย่เลย ขึ้นอยู่กับนักพัฒนาของเรา”
นี่เป็นหนึ่งในวิธีแก้ปัญหาที่ฉันโปรดปราน
ประเภทของข้อผิดพลาดใน Node.js
ก่อนอื่น จำเป็นต้องมีความเข้าใจที่ชัดเจนเกี่ยวกับข้อผิดพลาดใน Node.js โดยทั่วไป ข้อผิดพลาด Node.js แบ่งออกเป็นสองประเภทที่แตกต่างกัน: ข้อผิดพลาดใน การปฏิบัติงาน และ ข้อผิดพลาดของโปรแกรมเมอร์
- ข้อผิดพลาดในการปฏิบัติงาน แสดงถึงปัญหารันไทม์ซึ่งผลลัพธ์ที่คาดหวังและควรได้รับการจัดการอย่างเหมาะสม ข้อผิดพลาดในการปฏิบัติงานไม่ได้หมายความว่าแอปพลิเคชันนั้นมีข้อบกพร่อง แต่นักพัฒนาจำเป็นต้องจัดการกับปัญหาเหล่านี้อย่างรอบคอบ ตัวอย่างของข้อผิดพลาดในการปฏิบัติงาน ได้แก่ "หน่วยความจำไม่เพียงพอ" "อินพุตที่ไม่ถูกต้องสำหรับตำแหน่งข้อมูล API" เป็นต้น
- ข้อผิดพลาดของโปรแกรมเมอร์ แสดงถึงข้อบกพร่องที่ไม่คาดคิดในโค้ดที่เขียนไม่ดี หมายความว่าโค้ดมีปัญหาที่ต้องแก้ไขและเขียนโค้ดผิด ตัวอย่างที่ดีคือพยายามอ่านคุณสมบัติของ "undefined" ต้องเปลี่ยนรหัสเพื่อแก้ไขปัญหา นั่นเป็นข้อบกพร่องที่นักพัฒนาสร้างขึ้น ไม่ใช่ข้อผิดพลาดในการปฏิบัติงาน
ด้วยเหตุนี้ คุณจึงไม่ควรมีปัญหาในการแยกแยะระหว่างข้อผิดพลาดสองประเภทนี้: ข้อผิดพลาดในการปฏิบัติงานเป็นส่วนหนึ่งของแอปพลิเคชัน และข้อผิดพลาดของโปรแกรมเมอร์คือจุดบกพร่องที่เกิดจากนักพัฒนา คำถามเชิงตรรกะที่ตามมาคือ: "เหตุใดการแบ่งพวกเขาออกเป็นสองประเภทและจัดการกับพวกเขาจึงมีประโยชน์"
หากไม่มีความเข้าใจที่ชัดเจนเกี่ยวกับข้อผิดพลาด คุณอาจรู้สึกอยากเริ่มแอปพลิเคชันใหม่ทุกครั้งที่มีข้อผิดพลาดเกิดขึ้น เหมาะสมหรือไม่ที่จะรีสตาร์ทแอปพลิเคชันเนื่องจากข้อผิดพลาด "ไม่พบไฟล์" เมื่อผู้ใช้หลายพันคนเพลิดเพลินกับแอปพลิเคชันนี้ ไม่ได้อย่างแน่นอน.
แต่สิ่งที่เกี่ยวกับข้อผิดพลาดของโปรแกรมเมอร์? จะทำให้แอปพลิเคชันทำงานต่อไปเมื่อมีจุดบกพร่องที่ไม่รู้จักปรากฏขึ้นซึ่งอาจส่งผลให้เกิดก้อนหิมะที่ไม่คาดคิดในแอปพลิเคชันหรือไม่ ย้ำอีกครั้งว่าไม่แน่นอน!
ถึงเวลาจัดการกับข้อผิดพลาดอย่างเหมาะสม
สมมติว่าคุณมีประสบการณ์กับ async JavaScript และ Node.js บ้างแล้ว คุณอาจพบข้อเสียเมื่อใช้การเรียกกลับเพื่อจัดการกับข้อผิดพลาด พวกเขาบังคับให้คุณตรวจสอบข้อผิดพลาดไปจนถึงข้อผิดพลาดที่ซ้อนกัน ทำให้เกิดปัญหา "callback hell" ที่ฉาวโฉ่ ซึ่งทำให้ยากต่อการปฏิบัติตามขั้นตอนของโค้ด
การใช้คำสัญญาหรือ 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(); } }
การใช้อ็อบเจ็กต์ Error ในตัวของ 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' );
Centralized 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 แบบกำหนดเอง:
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<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 สำหรับการจัดการข้อผิดพลาดตามสัญญา