Hướng dẫn response error nodejs - lỗi phản hồi nodejs

Bất kỳ dự án nào cũng đều phải có một phần dành riêng cho việc xử lý các lỗi. Rất nhiều bạn khi tham gia dự án, không biết cách “quy hoạch” mã nguồn cho phần xử lý các lỗi một cách khoa học, code được clean.

Nếu xử lý lỗi đúng cách, nó làm giảm thời gian phát triển ứng dụng, giúp code base dễ dàng mở rộng.

Với các bạn mới học Node.JS, chắc chắn sẽ cảm thấy Node.JS thật khó học, khó làm việc vì nó quá bừa bộn. Bạn thấy lỗi ở đâu thì xử lý luôn tại đó, không cần theo một quy tắc nào cả. Bạn thốt nên rằng “NodeJS thật là tồi tệ, dự án mà phức tạp hơn thì mọi chuyện sẽ đi về đâu với đống code này chứ?”.

Câu trả lời là “Không, NodeJS không tệ như bạn nghĩ đâu. NodeJS xấu hay tốt hoàn toàn phụ thuộc vào bạn.”

Bài viết này mình sẽ giới thiệu cách xử lý error trong NodeJS sao cho khoa học, clean code.

Nội dung chính của bài viết

  • Những kiểu Error trong NodeJS
    • Ứng xử với mỗi loại Error như thế nào?
  • Xử lý Error trong NodeJS đúng cách
  • Node.js Error-handling tập trung
    • Workflow của Error Handling trong NodeJS
    • Sử dụng thư viện logger
    • Xử lý programmer errors
    • Xử lý unhandled promise rejections
    • Tóm lại

Trước khi đi vào phần chính, chúng ta cần phân biệt các kiểu ERROR trong NodeJS. Nói chung, error trong NodeJS được chia làm 2 loại:

  • Operational errors: là các vấn đề xảy ra trong quá trình ứng dụng hoạt động khi đầu vào không được bình thường. Thực ra đây không hẳn là lỗi của ứng dụng, chỉ là chúng ra phải xử lý chúng sao cho khéo, nếu không nó sẽ thành lỗi, gây ảnh hưởng trải nghiệm người dùng. Ví dụ: hết bộ nhớ lưu trữ, người dùng nhập dữ liệu không hợp lệ…là các vấn đề xảy ra trong quá trình ứng dụng hoạt động khi đầu vào không được bình thường. Thực ra đây không hẳn là lỗi của ứng dụng, chỉ là chúng ra phải xử lý chúng sao cho khéo, nếu không nó sẽ thành lỗi, gây ảnh hưởng trải nghiệm người dùng. Ví dụ: hết bộ nhớ lưu trữ, người dùng nhập dữ liệu không hợp lệ…
  • Programmer errors: đây là những lỗi không mong muốn xảy ra do chất lượng code không tốt. Một ví dụ điển hình là cố gắng access vào thuộc tính của đối tượng “undefined”. Đây là những lỗi do nhà phát triển gây ra, chứ không phải do người dùng hay môi trường.đây là những lỗi không mong muốn xảy ra do chất lượng code không tốt. Một ví dụ điển hình là cố gắng access vào thuộc tính của đối tượng “undefined”. Đây là những lỗi do nhà phát triển gây ra, chứ không phải do người dùng hay môi trường.

Ứng xử với mỗi loại Error như thế nào?

Xử lý Error trong NodeJS đúng cách

Node.js Error-handling tập trung

Workflow của Error Handling trong NodeJS

Sử dụng thư viện logger

Xử lý programmer errorsHướng dẫn tạo Secure REST API trong Node.js

Xử lý Error trong NodeJS đúng cách

Node.js Error-handling tập trung

Workflow của Error Handling trong NodeJS

Sử dụng thư viện logger

Xử lý programmer errors

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();
 }
}

Xử lý unhandled promise rejections

Tóm lại

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);
 }
}

Trước khi đi vào phần chính, chúng ta cần phân biệt các kiểu ERROR trong NodeJS. Nói chung, error trong NodeJS được chia làm 2 loại:

export enum HttpStatusCode {
 OK = 200,
 BAD_REQUEST = 400,
 NOT_FOUND = 404,
 INTERNAL_SERVER = 500,
}

Operational errors: là các vấn đề xảy ra trong quá trình ứng dụng hoạt động khi đầu vào không được bình thường. Thực ra đây không hẳn là lỗi của ứng dụng, chỉ là chúng ra phải xử lý chúng sao cho khéo, nếu không nó sẽ thành lỗi, gây ảnh hưởng trải nghiệm người dùng. Ví dụ: hết bộ nhớ lưu trữ, người dùng nhập dữ liệu không hợp lệ…

class HTTP400Error extends BaseError {
 constructor(description = 'bad request') {
   super('NOT FOUND', HttpStatusCode.BAD_REQUEST, true, description);
 }
}

Programmer errors: đây là những lỗi không mong muốn xảy ra do chất lượng code không tốt. Một ví dụ điển hình là cố gắng access vào thuộc tính của đối tượng “undefined”. Đây là những lỗi do nhà phát triển gây ra, chứ không phải do người dùng hay môi trường.vậy cách sử dụng các Error class trên như thế nào?

Có thể bạn sẽ thắc mắc:”Tại sao chúng ta phải chia ra hai loại lỗi, trong khi lỗi nào cũng phải xử lý”

...
const user = await User.getUserById(1);
if (user === null)
 throw new APIError(
   'NOT FOUND',
   HttpStatusCode.NOT_FOUND,
   true,
   'Giải thích chi tiết error'
 );

Node.js Error-handling tập trung

Workflow của Error Handling trong NodeJS

Sử dụng thư viện logger

Xử lý programmer errors

Workflow của Error Handling trong NodeJS

Sử dụng thư viện logger

Hướng dẫn response error nodejs - lỗi phản hồi nodejs

Xử lý programmer errors

...
try {
 userService.addNewUser(req.body).then((newUser: User) => {
   res.status(200).json(newUser);
 }).catch((error: Error) => {
   next(error)
 });
} catch (error) {
 next(error);
}
...

Error-handling middleware là nơi tốt nhất để phân loại các Error. Sau đó gửi chúng tới phần xử lý lỗi tập trung. Ở phần này, nếu bạn đã có kiến thức nhất định về middleware trong NodeJS thì lại quá tuyệt vời.

💦 Có ích cho bạn: Tài liệu học lập trình Node.js tiếng việt tốt nhấtTài liệu học lập trình Node.js tiếng việt tốt nhất

app.use(async (err: Error, req: Request, res: Response, next: NextFunction) => {
 if (!errorHandler.isTrustedError(err)) {
   next(err);
 }
 await errorHandler.handleError(err);
});

Đọc đến đây, chắc hẳn bạn đã hình dung phần nào về phần xử lý Error tập trung rồi đúng không?

Hãy nhớ rằng, việc triển khai cụ thể sẽ hoàn toàn phụ thuộc vào bạn, vào yêu cầu của dự án. Dưới đây chỉ là một ví dụ:

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();

Sử dụng thư viện logger

Đôi khi, nếu bạn thích sự cầu toàn, muốn log được in ra có định dạng gọn gàng, thông tin chi tiết, màu sắc sặc sỡ… Lúc này

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);
 }
}
1 không đáp ứng được.

Lời khuyên là bạn nên sử dụng một thư viện hỗ trợ in log, cụ thể là winston hoặc morgan.

Đây là một ví dụ sử dụng winston logger và chỉnh sửa theo ý muốn:

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();

Về cơ bản, những thư viện logger như winston hay morgan sẽ hỗ trợ bạn ghi log ở nhiều cấp độ khác nhau (Info, Debug, warning, Error) theo cách được định dạng trước, với màu sắc rõ ràng.

Ưu điểm mà mình tâm đắc nhất đó là có thể dùng công cụ phân tích log để phân tích các tệp log được định dạng. Nhờ đó, bạn có thêm các thông tin hữu ích về ứng dụng. Ví dụ: tần suất sử dụng ứng dụng, tỉ lệ lỗi, lỗi nào xảy ra nhiều nhất…

Xử lý programmer errors

Cho đến thời điểm này, chúng ta mới chỉ nói tới cách xử lý các operational errors. Vậy còn programmer errors thì sao?

Cách xử lý tốt nhất là khi gặp programmer errors thì xử lý ngay lập tức, ngay tại đoạn code bị lỗi và khởi động lại ứng dụng một cách khéo léo bằng trình quản lý process như PM2.

Còn lý tại sao lại phải khởi động lại ứng dụng khi gặp programmer errors!? Mình đã đề cập ở trên rồi nhé.

Đây là một ví dụ:

process.on('uncaughtException', (error: Error) => {
 errorHandler.handleError(error);
 if (!errorHandler.isTrustedError(error)) {
   process.exit(1);
 }
});

Phần tiếp theo, cũng không kém phần quan trọng. Đó là chúng ta sẽ xử lý các trường hợp unhandled promise rejections.

Xử lý unhandled promise rejections

Khi làm việc với Promise trong NodeJS, có lẽ không ít lần bạn bắt gặp các lỗi liên quan tới

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);
 }
}
2. Nguyên nhân là do bạn quên xử lý các rejections của Promise.

Như đã biết, mỗi Promise sẽ có 2 tham số: Resolve và Reject. Thường thì bạn code vội nên chỉ handle mỗi phần resolve thôi. Nhưng đến khi gặp lỗi thì Reject mới lòi ra, mà bạn lại không có code handle nó, thế là gặp lỗi thôi.ResolveReject. Thường thì bạn code vội nên chỉ handle mỗi phần resolve thôi. Nhưng đến khi gặp lỗi thì Reject mới lòi ra, mà bạn lại không có code handle nó, thế là gặp lỗi thôi.

Có một ý tưởng khá hay để xử lý trường hợp này. Đó là sử dụng một callback hợp lệ và đăng ký

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);
 }
}
3

Dưới đây là một ví dụ:

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);
 }
}
0

Tóm lại

Sau tất cả, qua bài viết này mình chỉ muốn nhắn nhủ với bạn rằng: Xử lý error trong NodeJS không phải là một khâu tùy chọn. Mà nó là bắt buộc trong việc phát triển ứng dụng.

Chiến lược xử lý tập trung tất cả các lỗi ở một nơi duy nhất trong NodeJS sẽ đảm bảo nhà phát triển tiết kiệm thời gian, code clean, dễ bảo trì, tránh trùng lặp code và ít bị ăn “chửi” từ các reviewer 😰

Mình hi vọng, bài viết xử lý error trong NodeJS này có ích với bạn. Đừng nỡ lòng đọc xong mà không để lại một bình luận động viên 😰.

🔥 Đọc thêm về NodeJS:

  • 7 sai lầm khi học Nodejs hay mắc phải
  • Hướng dẫn crawl dữ liệu website bằng NodeJS
  • Node.js Module là gì? Cách tạo và sử dụng module trong nodejs

Hướng dẫn response error nodejs - lỗi phản hồi nodejs