Sửa lỗi UnhandledPromiseRejectionWarning trong Node.js async/await

intermediate💚 Node.js2026-03-19| Node.js v10–v18+, mọi hệ điều hành (Linux, macOS, Windows)

Error Message

UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1)
#nodejs#promise#async#await#error-handling

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 async ném lỗi nhưng phía gọi không await nó trong try/catch
  • Một chuỗi .then() không có .catch() ở cuối
  • Một event handler hoặc callback gọi hàm async như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 UnhandledPromiseRejectionWarning phả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.

Related Error Notes