Một số phương pháp xử lý lỗi (error handling) trong Node.js

Một số phương pháp xử lý lỗi (error handling) trong Node.js

Vấn đề

Lỗi là một vấn đề tồn tại song song với việc phát triển ứng dụng, tôi hay nói vui là chừng nào còn code thì còn "bug". Lỗi cũng có lỗi "this" lỗi "that", có lỗi chúng ta hoàn toàn nhận biết được chúng sẽ xảy ra trong tương lai, bên cạnh đó là những lỗi trời ơi đất hỡi mà chúng ta hoàn toàn không biết khả năng xuất hiện của chúng.

Lỗi gây ra những phiền toái, đôi khi gây ra những hậu quả nghiêm trọng. Chính vì thế xử lý lỗi luôn là vấn đề quan trọng trong lập trình. Bài viết hôm nay tôi xin trình bày một số phương pháp xử lý lỗi một cách "duyên dáng" trong môi trường Node.js.

Các phương pháp

Sử dụng async/await hoặc promise để xử lý lỗi ở các hàm không đồng bộ

Tránh sử dụng callback để rơi vào callback hell khiến mã của bạn trở nên lồng nhau và gây rối mắt, khó khăn cho bảo trì.

Ví dụ một trường hợp sử dụng callback để xử lý lỗi:

getData(someParameter, function(err_a, a) {
    if(err_a!== null) {
        getMoreDataA(a, function(err_b, b) {
            if(err_b !== null) {
                getMoreDataB(b, function(err_c, c) {
                    getMoreDataC(c, function(err_d, d) {
                        if(err_d !== null ) {
                            // do something
                        }
                    })
                });
            }
        });
    }
});

Thay vào đó hãy sử dụng promise để xử lý lỗi một cách "duyên dáng" hơn:

return getData(someParameter)
    .then(getMoreDataA)
    .then(getMoreDataB)
    ...
    .catch(err => handle(err));

Tuy nhiên promise có gây khó khăn trong khi debug, hãy sử dụng cú pháp "thanh lịch" của async/await kết hợp với try/catch:

try {
    const a = await getData(someParameter);
    const b = await getData(someParameter);
} catch(err) {
    handle(err);
}

Trả ra một đối tượng Error thay vì bất kì đối tượng có kiểu nào khác khi muốn báo lỗi.

Vì tính dễ dãi của JS chúng ta có thể "throw" ra tuỳ ý một đối tượng nào để báo lỗi, nó có thể là một số, một chuỗi hay một object nhưng đừng nên làm như thế. Hãy throw ra một Error để đảm bảo tính đồng nhất mã của bạn và với các thư viện, hơn nữa Error còn lưu giữ thông tin quan trọng như StackTrace là vị trí gây ra lỗi.

/ / đừng làm thế này
if (!condition) {
    throw ('điều kiện không hợp lệ');
}

// mà hãy làm thế này
if (!condition) {
    throw new Error('điều kiện không hợp lệ');
}

// hay thậm chí bạn có thể làm tốt hơn bằng cách tạo ra một đối tượng lỗi
// mang thêm nhiều thông tin hữu ích khác
function MyError(name, description, ...args) {
    Error.call(this);
    Error.captureStackTrace(this);
    this.name = name;
    this.description = description;
    ...
};

MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;

if (!condition) {
    throw new MyError('CONDITION_NOT_VALID', 'điều kiện không hợp lệ');
}

Xử lý lỗi tập trung

Tạo ra một hoặc nhiều hàm sẵn sàng nhận vào một đối tượng lỗi sau đó thì phân tích hay phân phát lỗi đến các nơi khác, tránh xử lý riêng lẻ khiến cho mã của bạn trở nên khó kiểm soát.

Tưởng tượng bạn sẽ làm gì khi có lỗi xảy ra? Log ra console, gửi chúng đến các dịch vụ theo dõi (tracking) hay logging, ghi ra file, kiểm tra điều kiện và phân loại lỗi… rất nhiều thứ cần làm với một lỗi được bắn ra vì thế hãy tập hợp chúng lại trong một hoặc nhiều hàm chuyên để xử lý lỗi.

Thận trọng với lỗi unhandledRejection

Đây là một lỗi rất khó chịu trong Node.js, nó có thể khiến cho ứng dụng của bạn bị treo và không thể tiếp tục xử lý bất kì yêu cầu nào được nữa. Lỗi này xảy ra khi bạn xử lý Promise không đúng cách, cụ thể là không có hàm xử lý lỗi của reject trong Promise.

UserModel.findByPk(1).then((user) => {
  if(!user)
      throw new Error('user not found');
});

Để bắt được lỗi này, chúng ta cần sử dụng process.on.

process.on('unhandledRejection', (reason, p) => {
  throw reason;
});

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

process.on thường được tạo ngay index.js nơi mà ứng dụng của bạn khởi động đầu tiên để nó có thể "lắng nghe" bất kì tín hiệu nào từ việc xử lý Promise không đúng cách. Để từ đó có phương án xử lý thích hợp tránh máy chủ bị treo.

Đừng bao giờ tin dữ liệu đầu vào, luôn xác thực chúng.

Chúng ta cung cấp những endpoint POST, PUT… để cho phép client truyền dữ liệu lên, có một điều là dữ liệu không thể đảm bảo luôn luôn đúng. Vì thế cách an toàn nhất là luôn validate dữ liệu nhận được, ngăn chặn hành vi vô tình hay cố ý gửi sai dữ liệu mong muốn có thể gây ra lỗi hay cố tình phá hoại hệ thống.

Có nhiều thư viện rất tốt giúp chúng ta làm điều này như joi hay là ajv.

const schema = Joi.object({
  username: Joi.string()
    .alphanum()
    .min(3)
    .max(30)
    .required(),

  password: Joi.string()
    .pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')),
});

let body = { username: '2coffee', password: '2coffee' };
schema.validate(body); // -> { value: { username: '2coffee', password: '2coffee' } }

let body = {};
schema.validate(body); // -> { value: {}, error: '"username" is required' }

Sử dụng trình ghi log chuyên nghiệp

Ghi log là một cách có thể theo dõi và giám sát những lỗi đã xảy ra trong quá khứ để có thể tra cứu lại bất kì lúc nào. Tôi có một bài viết riêng nói về việc ghi log trong ứng dụng Node.js, bạn đọc xem thêm tại Logging ứng dụng viết bằng node.js qua 3 cấp độ.

Hãy viết Unit test

Unit test là một trong những phương pháp giúp bạn phát hiện ra lỗi sớm nhất trong quá trình phát triển. Tuy mất nhiều thời gian để viết Unit test nhưng nó rất đáng giá để đầu tư thời gian của bạn. Một khi Unit test hoạt động tốt nó sẽ tiết kiệm cho bạn rất nhiều thời gian và chi phí sau này đấy.

"Thoát" ứng dụng khi có thể

Trong trường hợp đối diện với những lỗi đặc thù trước mắt chưa có cách nào chữa trị hoặc bắt được lỗi nhưng khó có thể khôi phục trạng thái ứng dụng thì cách xử lý tạm thời là hãy khiến cho ứng dụng bị crash và nhờ các công cụ DevOps khởi động lại. Đây có thể không phải là cách hay nhưng việc khởi động lại ứng dụng của bạn sẽ khôi phục lại được trạng thái trước khi xảy ra lỗi.

Sử dụng AMP

Application Performance Management (APM) được dùng để giám sát và quản lý hiệu suất cũng như theo dõi tính khả dụng của ứng dụng. APM cố gắng phát hiện và chuẩn đoán các vấn đề về hiệu suất ứng dụng và kịp thời thông báo cho bạn biết khi có vấn đề gì xảy ra.

Hiện tại có khá nhiều sản phẩm, dịch vụ APM trên thị trường từ Open Source, miễn phí đến trả phí. Tính năng cũng rất phong phú từ đơn giản đến phức tạp, theo dõi bằng việc "ping" hoặc tích hợp sâu vào theo dõi hệ thống phức tạp.

Hãy bắt đầu với một dịch vụ APM đơn giản như uptimerobot.com. Nó theo dõi thời gian uptime ứng dụng bằng cách gửi một yêu cầu sau mỗi 5 phút và chờ phản hồi thành công hay thất bại.

banner

Newrelic là một công cụ theo dõi toàn diện hơn vì nó theo dõi được nhiều thứ hơn như ngăn xếp lỗi chi tiết, thời gian phản hồi, nút thắt cổ chai và thống kê chi tiết…

banner

Tổng kết

Trên đây là một số phương pháp xử lý lỗi đối với ứng dụng viết bằng Node.js mà tôi đề xuất. Có thể ngoài những cách trên còn có nhiều phương pháp khác mà tôi chưa biết, nếu bạn phát hiện ra còn thiếu xin hãy để lại dưới phần bình luận.

hoặc
* Bản tin tổng hợp được gửi mỗi 1-2 tuần, huỷ bất cứ lúc nào.
Author

Xin chào, tôi tên là Hoài - một anh Dev kể chuyện bằng cách viết ✍️ và làm sản phẩm 🚀. Với nhiều năm kinh nghiệm lập trình, tôi đã đóng góp một phần công sức cho nhiều sản phẩm mang lại giá trị cho người dùng tại nơi đang làm việc, cũng như cho chính bản thân. Sở thích của tôi là đọc, viết, nghiên cứu... Tôi tạo ra trang Blog này với sứ mệnh mang đến những bài viết chất lượng cho độc giả của 2coffee.dev.Hãy theo dõi tôi qua các kênh LinkedIn, Facebook, Instagram, Telegram.

Bạn thấy bài viết này có ích?
Không

Bình luận (1)

Avatar
Jess Vanes2 năm trước
Hướng dẫn dùng mấy tool apm đi ạ
Trả lời
Avatar
Xuân Hoài Tống2 năm trước
@gif [ISOckXUybVfQ4] Chắc là sẽ hơi lâu ạ