Chuyện gì đã xảy ra
Bạn đang chạy ứng dụng Node.js và đột nhiên gặp lỗi này:
TypeError: Cannot read properties of undefined (reading 'propertyName')
at Object.<anonymous> (/app/index.js:12:20)
at Module._compile (node:internal/modules/cjs/loader:1364:14)
Biến bạn đang đọc có giá trị undefined — không phải null, không phải object rỗng, mà hoàn toàn không tồn tại. Node.js 16+ đưa ra thông báo lỗi rõ ràng hơn; các phiên bản cũ chỉ hiện "Cannot read property 'x' of undefined".
Nguyên nhân thường gặp
- Truy cập thuộc tính lồng nhau trước khi object cha được khởi tạo
- Một hàm async trả về
undefinedvì bạn quênawait - Tham số hàm không được truyền vào tại nơi gọi
- Truy cập phần tử mảng ở chỉ số vượt ngoài phạm vi
- Khóa được destructure không tồn tại trong object nguồn
- Thiếu câu lệnh
returntrong hàm tạo ra giá trị đó
Quy trình debug
Bước 1 — Đọc stack trace
Stack trace cho bạn biết chính xác file và dòng xảy ra lỗi. Hãy xem đó trước. Ví dụ:
TypeError: Cannot read properties of undefined (reading 'email')
at sendWelcome (/app/services/user.js:24:30)
Mở file user.js dòng 24 và xem biến nằm bên trái dấu .email là gì.
Bước 2 — Log giá trị trước khi truy cập
Thêm một console.log ngay trên dòng gây lỗi:
// user.js dòng 23
console.log('user object:', user);
const email = user.email; // dòng 24 — bị crash ở đây
Nếu log in ra undefined, nghĩa là biến chưa bao giờ được gán giá trị.
Bước 3 — Kiểm tra thiếu await
Thiếu await là nguyên nhân của một phần lớn các lỗi này trong code async. Không có nó, bạn nhận được một Promise object — chứ không phải giá trị đã resolved:
// Lỗi: quên await
const user = getUser(id); // trả về Promise, không phải user
console.log(user.email); // TypeError
// Sửa
const user = await getUser(id);
console.log(user.email); // hoạt động đúng
Bước 4 — Truy ngược nguồn gốc giá trị
Truy ngược lại. Nếu user là undefined, user đến từ đâu? Một query database? Một response API? Một đối số hàm? Kiểm tra từng mắt xích trong chuỗi.
// Query database trả về null/undefined
const user = await db.users.findOne({ id });
// Nếu không tìm thấy bản ghi, user là null hoặc undefined
console.log(user.email); // TypeError
Cách khắc phục
Cách 1 — Dùng optional chaining (Node.js 14+)
Toán tử ?. sẽ dừng lại và trả về undefined thay vì ném lỗi, giúp đọc các thuộc tính lồng nhau có thể không tồn tại một cách an toàn:
// Thay vì:
const city = user.address.city;
// Dùng:
const city = user?.address?.city;
console.log(city); // undefined (không crash)
Phù hợp nhất khi chỉ đọc dữ liệu và giá trị bị thiếu là hành vi chấp nhận được.
Cách 2 — Kiểm tra tồn tại tường minh
Nếu dữ liệu bị thiếu là điều kiện lỗi, hãy báo lỗi rõ ràng với status code phù hợp:
const user = await db.users.findOne({ id });
if (!user) {
return res.status(404).json({ error: 'Không tìm thấy người dùng' });
}
// An toàn từ đây trở đi
console.log(user.email);
Cách 3 — Giá trị mặc định với nullish coalescing
Kết hợp ?. với ?? để dùng giá trị dự phòng khi object config bị thiếu hoàn toàn:
const config = loadConfig();
// Nếu config là undefined, dùng giá trị mặc định
const timeout = config?.timeout ?? 3000;
const retries = config?.retries ?? 3;
Cách 4 — Sửa thiếu return trong hàm
Dễ bỏ qua, đặc biệt trong các hàm có nhiều nhánh điều kiện:
// Lỗi: một nhánh quên return
function getUser(id) {
if (id > 0) {
return { id, name: 'Alice' };
}
// ngầm trả về undefined khi id 0) {
return { id, name: 'Alice' };
}
return null; // hoặc ném lỗi
}
Cách 5 — Destructuring với giá trị mặc định
Khi người gọi bỏ qua một đối số, các thuộc tính được destructure sẽ crash ngay lập tức. Hãy đặt giá trị mặc định an toàn cho tham số:
// Lỗi
function process({ name, options }) {
console.log(options.timeout); // TypeError nếu options không được truyền
}
// Sửa — đặt mặc định cho toàn bộ tham số hoặc các khóa lồng nhau
function process({ name, options = {} }) {
console.log(options.timeout); // undefined, không crash
}
Cách 6 — Kiểm tra giới hạn mảng
Truy cập chỉ số 0 trên mảng rỗng trả về undefined. Kiểm tra độ dài trước, hoặc dùng optional chaining:
const items = [];
// Lỗi
console.log(items[0].name); // TypeError
// Sửa
if (items.length > 0) {
console.log(items[0].name);
}
// Hoặc dùng optional chaining
console.log(items[0]?.name);
Kiểm tra kết quả
Chạy lại ứng dụng và xác nhận lỗi không còn xuất hiện:
# Chạy ứng dụng hoặc bộ test
node index.js
# Hoặc chạy test
npm test
Optional chaining âm thầm bỏ qua các giá trị bị thiếu. Nếu bạn dùng nó như một fallback, hãy thêm assertion để xác nhận bạn đang nhận được dữ liệu thực — chứ không chỉ là undefined mà không bị crash:
const email = user?.email;
console.assert(email !== undefined, 'Kỳ vọng user.email phải được gán giá trị');
Bài học rút ra
- Đừng bao giờ giả định hàm async đã trả về thành công — luôn kiểm tra kết quả trước khi truy cập thuộc tính.
- Optional chaining không phải thuốc chữa bách bệnh — nó che giấu lỗi, nhưng giá trị vẫn bị thiếu. Dùng khi giá trị vắng mặt là điều bình thường; dùng guard khi vắng mặt là bug.
- TypeScript phát hiện hầu hết các lỗi này trước khi chạy — nếu bạn đang bắt đầu một service Node.js mới, hãy thêm TypeScript. Nó cảnh báo
user.emailkhiusercó thể làundefinedngay lúc biên dịch, chứ không phải lúc 2 giờ sáng trên môi trường production. - Stack trace là người bạn đồng hành của bạn — file và dòng lỗi luôn hiển thị ở đó. Hãy đọc nó trước khi đoán mò.

