Chuyện gì đang xảy ra
Ứng dụng Node.js của bạn in ra thứ gì đó như thế này trong console:
UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1)
UnhandledPromiseRejectionWarning: Error: connect ECONNREFUSED 127.0.0.1:5432
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1144:16)
(node:12345) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated.
In future versions of Node.js, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Có gì đó âm thầm thất bại — kết nối DB, gọi API, đọc file. Ứng dụng tiếp tục chạy như không có gì xảy ra. Trong Node.js v15+, điều này thực sự làm crash tiến trình ngay lập tức. Ở các phiên bản cũ hơn, nó chỉ in cảnh báo này rồi tiếp tục — cái đó còn tệ hơn. Lỗi âm thầm là loại bug khó truy vết nhất trên môi trường production.
Tại sao điều này xảy ra
Một Promise bị reject ở đâu đó trong code nhưng không có gì bắt nó. Ba pattern sau gây ra vấn đề này hầu hết thời gian:
- Một hàm
asyncném lỗi nhưng phía gọi khôngawaitnó trongtry/catch - Một chuỗi
.then()không có.catch()ở cuối - Một event handler hoặc callback gọi hàm
asyncnhưng bỏ qua Promise được trả về
Tìm nguồn gốc lỗi
Bắt đầu bằng cách lấy stack trace hữu ích. Chạy Node với flag --trace-warnings:
node --trace-warnings app.js
Lệnh này in toàn bộ stack trace trỏ đến đúng dòng code nơi rejection bắt nguồn. Nếu không có flag này, các phiên bản Node cũ chỉ hiển thị thông báo rejection — bạn không có manh mối nào về nguồn gốc của nó.
Một cách khác: thêm global handler ở đầu file entry trong khi debug:
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason);
});
Đặt nó ở dòng 1 của index.js hoặc bất cứ nơi nào ứng dụng khởi động. Nó bắt mọi rejection bị bỏ sót. Hữu ích để tìm ra vấn đề — nhưng đừng coi đây là giải pháp lâu dài.
Cách sửa
Cách 1: Bọc async/await trong try/catch
Chín trong mười trường hợp, thủ phạm trông như thế này — một hàm async được gọi mà không có xử lý lỗi nào:
// Lỗi — rejection không được xử lý
async function fetchUser(id) {
const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
return user;
}
fetchUser(123); // Không có await, không có .catch()
Bọc phần bên trong bằng try/catch, rồi xử lý ở phía gọi hàm nữa:
// Đã sửa — lỗi được bắt
async function fetchUser(id) {
try {
const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
return user;
} catch (err) {
console.error('Failed to fetch user:', err.message);
throw err; // Ném lại để phía gọi biết có lỗi xảy ra
}
}
// Xử lý ở phía gọi hàm
try {
const user = await fetchUser(123);
} catch (err) {
// Xử lý hoặc ghi log
}
Cách 2: Thêm .catch() vào chuỗi Promise
Đang dùng chuỗi .then() thay vì async/await? Mỗi chuỗi cần có .catch() ở cuối:
// Lỗi — rejection âm thầm biến mất
fetch('https://api.example.com/data')
.then(res => res.json())
.then(data => process(data));
// Thiếu .catch()
// Đã sửa
fetch('https://api.example.com/data')
.then(res => res.json())
.then(data => process(data))
.catch(err => {
console.error('API call failed:', err.message);
});
Cách 3: Xử lý hàm async bên trong event handler
Cái này hay làm người ta bị bất ngờ. Event emitter và callback không hiểu Promise, nên lỗi trong các async callback âm thầm thoát ra ngoài:
// Lỗi — route Express
app.get('/users/:id', async (req, res) => {
const user = await fetchUser(req.params.id); // ném lỗi? Express không bao giờ biết
res.json(user);
});
// Đã sửa — bọc bằng try/catch và chuyển tiếp đến next()
app.get('/users/:id', async (req, res, next) => {
try {
const user = await fetchUser(req.params.id);
res.json(user);
} catch (err) {
next(err); // Chuyển đến Express error handler
}
});
Viết boilerplate này trên mỗi route rất mệt mỏi. Một utility wrapper nhỏ sẽ gọn hơn nhiều:
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 fetchUser(req.params.id);
res.json(user);
}));
Cách 4: Global handler như một lưới an toàn (production)
Dù code của bạn đã xử lý lỗi chắc chắn, vẫn đáng thêm một global fallback. Đây là phòng thủ cuối cùng:
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Promise Rejection:', reason);
// Ghi log lên dịch vụ monitoring (Sentry, Datadog, v.v.)
// sau đó tùy chọn tắt gracefully
// process.exit(1);
});
Trên Node.js v15+, tiến trình đã thoát khi có unhandled rejection theo mặc định. Handler này cho bạn thời gian để ghi log lỗi và dọn dẹp trước khi điều đó xảy ra.
Kiểm tra sau khi sửa
Chạy ứng dụng và cố tình kích hoạt đoạn code đã gây ra cảnh báo:
node --trace-warnings app.js
- Dòng
UnhandledPromiseRejectionWarningphải biến mất - Nếu điều kiện lỗi vẫn xảy ra, bạn sẽ thấy lỗi đã được bắt đúng cách trong log thay vì cảnh báo
- Trên Node.js v15+, ứng dụng của bạn sẽ không còn crash bất ngờ nữa
Viết một test nhanh để ép rejection xảy ra và xác nhận nó được xử lý:
// Ép rejection để kiểm tra handler của bạn hoạt động đúng
async function test() {
try {
await Promise.reject(new Error('test rejection'));
} catch (err) {
console.log('Caught expected error:', err.message); // Dòng này phải được in ra
}
}
test();
Bài học rút ra
Mọi lời gọi hàm async đều cần xử lý lỗi — không có ngoại lệ. Hoặc dùng try/catch quanh await, hoặc .catch() trên Promise được trả về. Những chỗ dễ bị bỏ sót là các hàm async được gọi từ ngữ cảnh không phải async: event handler, callback của setTimeout, stream listener. Những thứ đó không quan tâm đến Promise bị reject của bạn.
Đang dùng Node.js v14 hoặc cũ hơn? Đừng bỏ qua những cảnh báo này chỉ vì tiến trình vẫn chạy. Hãy nâng cấp lên v15+ và Node sẽ buộc bạn phải sửa — tốt hơn là phải truy vết bug data corruption âm thầm lúc 2 giờ sáng trên production.
Giữ --trace-warnings trong bộ công cụ debug của bạn. Ngay khi thấy một cảnh báo không rõ nguồn gốc, flag đó sẽ tìm ra nó trong vài giây.

