Menu

Xử lý lỗi trong Express: error middleware và lỗi async

Error handler trong Express là một middleware nhận bốn đối số, (err, req, res, next), được đăng ký sau các route. Express 5 chuyển đến đó lỗi bị ném ra, các lệnh gọi next(err) và promise bị reject từ handler async.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Express xử lý lỗi bằng một middleware đặc biệt nhận bốn đối số, (err, req, res, next), và được đăng ký sau tất cả các route. Express chuyển đến đó mọi lỗi bị ném ra trong route, mọi lỗi bạn truyền vào next(err), và ở Express 5 là cả mọi promise bị reject từ handler async. Handler này quyết định status code và gửi một response lỗi duy nhất, nên phần còn lại của code chỉ cần ném lỗi (throw).

GET /users/99 trả về 404 với {"error":"No user with id 99"}, và Console hiện dòng log từ error handler. Gửi GET /users/1 thì route trả lời bình thường; error handler chỉ chạy khi có gì đó sai.

Những lỗi Express tự bắt giúp bạn

Có ba cách để đi từ một route đến error handler, và ở Express 5 cả ba đều hoạt động như nhau:

GET /async trả về 500 với {"error":"Rejected in an async route"}; GET /sync và GET /next cũng kết thúc ở cùng handler đó với thông báo riêng của từng route. Giờ hãy xóa next khỏi danh sách tham số của error handler rồi gửi lại GET /sync: với ba tham số, Express coi nó là middleware thường, bỏ qua nó và trả về trang lỗi HTML mặc định.

Lỗi async: Express 5 và Express 4

Route /async chạy được là vì Express 5 bắt được promise bị reject. Express 4 thì không: lỗi trở thành unhandled rejection và request bị treo.

Express 4Express 5
throw trong handler đồng bộBắt đượcBắt được
next(err)Bắt đượcBắt được
Promise bị reject từ handler asyncKhông bắt được, request bị treoBắt được
Lỗi trong callback của setTimeout hoặc sự kiệnKhông bắt được, tiến trình bị crashKhông bắt được, tiến trình bị crash

Với Express 4, hãy bọc từng handler async hoặc cài package express-async-errors:

// Express 4 only
const asyncHandler = (fn) => (req, res, next) =>
    Promise.resolve(fn(req, res, next)).catch(next);

app.get('/users/:id', asyncHandler(async (req, res) => {
    const user = await db.findUser(req.params.id);
    res.json(user);
}));

Hàng cuối cùng giống nhau ở cả hai phiên bản: Express không thể thấy lỗi bị ném ra trong một callback mà nó không gọi. Hãy chuyển các API dạng callback sang promise (phần lớn Node đều có bản promise, chẳng hạn fs/promises) rồi await chúng.

Handler 404 và lỗi JSON thống nhất

API dễ dùng hơn khi mọi lỗi có cùng một dạng. Gắn status cho lỗi, biến các route không khớp thành lỗi 404, và để một handler định dạng tất cả:

GET /nope không khớp route nào, nên handler 404 biến nó thành lỗi và câu trả lời là {"error":"Cannot GET /nope","status":404}. Gửi GET /users/99 thì lỗi do route ném ra cũng có cùng dạng. Gửi POST /users với body {"name": }: express.json() không parse được và gọi next() với một lỗi có status là 400, nên câu trả lời là {"error":"Unexpected token '}', \"{\"name\": }\" is not valid JSON","status":400}.

Đừng để lộ stack trace

Thông báo lỗi từ HttpError của bạn được viết cho client đọc. Thông báo và stack từ mọi thứ khác (driver cơ sở dữ liệu, một lỗi gõ nhầm ném ra TypeError) có thể làm lộ đường dẫn tệp, câu truy vấn và phiên bản thư viện. Handler ở trên gửi Internal server error cho mọi lỗi 5xx và ghi lỗi thật vào log trên server. Dòng if (res.headersSent) return next(err); xử lý lỗi xảy ra khi một phần response đã được gửi đi: lúc đó cách an toàn duy nhất là để Express đóng kết nối.

Handler mặc định của Express dựa vào NODE_ENV: nó đưa stack trace vào trang HTML trừ khi NODE_ENV là production. Hãy đặt biến này trên server production (NODE_ENV=production node server.js), nhưng đừng chỉ trông vào nó: một handler tự viết không bao giờ gửi err.stack sẽ an toàn hơn.

Lỗi thường gặp

Đăng ký error handler trước các route. Express chỉ chuyển lỗi về phía sau, nên một handler đặt phía trên route sẽ không bao giờ thấy lỗi của route đó. Đặt nó ở cuối cùng, sau handler 404.

Bắt lỗi rồi không làm gì cả. Một khối try/catch chỉ ghi log sẽ để request bị treo. Trong catch, hãy gọi next(err), ném lỗi lại, hoặc gửi response.

Gửi hai response. Ném lỗi sau khi đã gọi res.json() khiến error handler cố gửi thêm lần nữa. Kiểm tra res.headersSent như trong ví dụ.

Để hiểu middleware thường và next() hoạt động thế nào, xem middleware trong Express.

Câu hỏi thường gặp

Express có thể có nhiều error handler không?

Có. Chúng chạy theo thứ tự, và mỗi handler hoặc gửi response, hoặc gọi next(err) để chuyển lỗi tiếp. Một cách chia phổ biến là một handler ghi log và một handler gửi response.

Chuyện gì xảy ra nếu chính error handler ném lỗi?

Express chuyển lỗi mới đó đến error handler tiếp theo, và nếu không còn handler nào thì đến handler có sẵn của nó, handler này gửi một trang 500. Hãy giữ error handler thật đơn giản để bản thân nó không thể lỗi.

Response lỗi nên dùng status code nào?

Dùng 4xx khi client gửi sai (400 dữ liệu không hợp lệ, 401 chưa đăng nhập, 403 không có quyền, 404 không tìm thấy, 409 xung đột) và 500 khi code của bạn bị lỗi. Đừng bao giờ trả về 200 kèm lỗi trong body.

Làm sao trả về lỗi validation cho nhiều trường cùng lúc?

Trả về 400 kèm một danh sách, ví dụ { "errors": [{ "field": "email", "message": "is required" }] }. Nếu dùng zod, hãy chuyển result.error.issues về dạng đó trong error handler để mọi route báo lỗi dữ liệu đầu vào theo cùng một cách.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu