Một ứng dụng Node.js chạy ổn trên máy lập trình viên có thể sập ngay trong tuần đầu lên production chỉ vì một Promise bị reject mà không ai bắt, hoặc một lỗi kết nối database bị nuốt mất khiến người dùng nhận về trang trắng. Xử lý lỗi thường bị xem là phần "làm sau", nhưng thực tế nó quyết định ứng dụng của bạn đáng tin cậy đến đâu. Bài viết này tổng hợp những nguyên tắc và mẫu code giúp xử lý lỗi trong Node.js một cách bài bản.
Hai loại lỗi cần phân biệt
Bước đầu tiên là hiểu rằng không phải lỗi nào cũng giống nhau:
- Lỗi vận hành (operational error): những tình huống có thể lường trước trong một hệ thống đang chạy đúng, như người dùng nhập sai dữ liệu, không tìm thấy bản ghi, API bên thứ ba timeout, mất kết nối mạng. Những lỗi này cần được xử lý và phản hồi một cách lịch sự.
- Lỗi lập trình (programmer error): bug trong code, như đọc thuộc tính của
undefined, gọi hàm sai kiểu tham số. Cách xử lý đúng là sửa code, chứ không phải bọctry/catchđể che đi.
Phân biệt hai loại này giúp bạn quyết định: lỗi vận hành thì trả về thông báo phù hợp và tiếp tục chạy; lỗi lập trình nghiêm trọng thì ghi log đầy đủ và cân nhắc khởi động lại tiến trình, vì trạng thái ứng dụng có thể đã không còn đáng tin.
Luôn ném đối tượng Error
Hãy luôn ném instance của Error hoặc lớp con của nó, không ném chuỗi hay object thường. Đối tượng Error mang theo stack trace, thứ vô giá khi debug. Từ Node.js 16.9 trở lên, bạn có thể dùng tùy chọn cause để giữ lại lỗi gốc khi bọc lỗi:
try {
await db.query(sql);
} catch (err) {
throw new Error("Không thể lưu đơn hàng", { cause: err });
}Tạo lớp lỗi tùy chỉnh
Một lớp lỗi riêng giúp gắn thêm thông tin như mã HTTP, mã lỗi nghiệp vụ và cờ cho biết lỗi có phải loại vận hành hay không:
class AppError extends Error {
constructor(message, { statusCode = 500, code = "INTERNAL", cause } = {}) {
super(message, { cause });
this.name = "AppError";
this.statusCode = statusCode;
this.code = code;
this.isOperational = true;
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`Không tìm thấy ${resource}`, { statusCode: 404, code: "NOT_FOUND" });
}
}Nhờ vậy, tầng xử lý chung chỉ cần kiểm tra instanceof AppError để biết nên phản hồi thế nào.
Xử lý lỗi bất đồng bộ
Phần lớn code Node.js là bất đồng bộ, và đây là nơi lỗi dễ bị "rơi" nhất.
- Với
async/await, dùngtry/catchtại nơi bạn thật sự xử lý được lỗi, không cần bọc mọi dòng. - Với chuỗi Promise, luôn có
.catch()ở cuối hoặc trả Promise về cho bên gọi. - Với API kiểu callback cũ, kiểm tra tham số
errđầu tiên trước khi dùng kết quả, hoặc chuyển sang phiên bản Promise nhưfs/promises. - Với
EventEmittervà stream, đăng ký listener cho sự kiệnerror. Nếu một sự kiệnerrorđược phát ra mà không có listener, Node.js sẽ ném lỗi và có thể làm sập tiến trình.
Middleware xử lý lỗi tập trung trong Express
Thay vì xử lý lỗi rải rác trong từng route, hãy đưa về một nơi duy nhất. Từ Express 5, nếu một route handler async ném lỗi hoặc trả về Promise bị reject, Express tự động chuyển lỗi đó cho next. Với Express 4, bạn cần tự gọi next(err) hoặc dùng hàm bọc.
app.get("/products/:id", async (req, res) => {
const product = await productService.findById(req.params.id);
if (!product) throw new NotFoundError("sản phẩm");
res.json(product);
});
app.use((err, req, res, next) => {
logger.error(err);
const status = err instanceof AppError ? err.statusCode : 500;
const message = err instanceof AppError ? err.message : "Đã có lỗi xảy ra";
res.status(status).json({ code: err.code ?? "INTERNAL", message });
});Lưu ý middleware xử lý lỗi phải có đủ bốn tham số và được khai báo sau cùng. Đừng bao giờ gửi stack trace hay thông điệp lỗi nội bộ cho client ở môi trường production, vì nó có thể làm lộ cấu trúc hệ thống.
Lưới an toàn cuối cùng
Node.js phát ra sự kiện unhandledRejection khi một Promise bị reject mà không được xử lý, và uncaughtException khi một lỗi đồng bộ không được bắt. Ở các phiên bản Node.js hiện đại, unhandled rejection mặc định sẽ làm tiến trình dừng lại.
process.on("unhandledRejection", (reason) => {
logger.fatal({ reason }, "Unhandled rejection");
process.exit(1);
});Hãy coi các listener này là lưới an toàn để ghi log, không phải cách để "nuốt" lỗi rồi chạy tiếp. Sau một uncaughtException, tài liệu Node.js khuyến nghị không tiếp tục chạy bình thường. Cách làm phổ biến là ghi log, đóng kết nối một cách êm ái rồi thoát, để trình quản lý tiến trình như PM2, systemd hay Kubernetes khởi động lại một phiên bản sạch.
Ghi log có cấu trúc
Lỗi chỉ có giá trị khi bạn tìm lại được nó. Một vài gợi ý:
- Dùng thư viện log có cấu trúc dạng JSON như pino hoặc winston thay vì
console.log. - Gắn request ID vào mỗi log để lần theo một request qua nhiều service.
- Phân cấp mức độ:
info,warn,error,fatal, và cấu hình cảnh báo cho mức nghiêm trọng. - Kết hợp công cụ theo dõi lỗi như Sentry để được thông báo ngay khi lỗi mới xuất hiện.
Kết luận
Xử lý lỗi đúng cách trong Node.js xoay quanh vài nguyên tắc: phân biệt lỗi vận hành với lỗi lập trình, luôn ném đối tượng Error có ngữ cảnh, không để Promise nào bị bỏ rơi, gom việc phản hồi lỗi về một middleware tập trung và ghi log đủ chi tiết. Đầu tư cho những điều này từ sớm sẽ giúp ứng dụng ổn định hơn và giúp team ngủ ngon hơn mỗi khi có sự cố.






