TL;DR
Hai file đang import lẫn nhau. Khi Node.js tải chúng, một module cố kế thừa từ một class chưa được export — nên nhận về undefined thay vì class thực. Cách khắc phục: tách class cha dùng chung ra một file riêng và import từ đó.
Lỗi trông như thế nào
TypeError: Class extends value undefined is not a constructor or null
at Object.<anonymous> (/app/models/Dog.js:3:19)
at Module._compile (node:internal/modules/cjs/loader:1356:14)
Stack trace luôn trỏ đến dòng class Foo extends Bar. Bar là undefined khi dòng đó chạy — không phải vì bạn quên export, mà vì file export nó chưa tải xong.
Nguyên nhân gốc rễ: vòng tròn thứ tự tải module
Các module CommonJS được cache ngay từ lúc bắt đầu tải. Khi hai file import lẫn nhau, một file nhận phiên bản chưa hoàn chỉnh của file kia. Đây là đúng pattern gây ra lỗi:
// Animal.js
const { Dog } = require('./Dog'); // ← tải Dog trước
class Animal {}
module.exports = { Animal };
// Dog.js
const { Animal } = require('./Animal'); // ← Animal là {} ở đây — chưa được export
class Dog extends Animal {} // TypeError: Class extends value undefined
module.exports = { Dog };
Thứ tự diễn ra như sau: Node bắt đầu tải Dog.js, điều này kích hoạt việc tải Animal.js. Ngay lập tức, Animal.js require Dog.js — nhưng Dog.js đang trong quá trình tải dở trong cache, nên Node trả về những gì đã được export đến thời điểm đó: một object rỗng {}. Destructuring { Animal } từ object rỗng đó cho ra undefined. Lỗi bùng phát ngay khi extends được thực thi.
Cách khắc phục 1: Tách class cha ra file riêng (khuyến nghị)
Chuyển class cha vào một file mà không có class con nào phụ thuộc vào. Rõ ràng, gọn gàng, luôn hoạt động.
// base/Animal.js ← file mới, không có circular import
class Animal {
speak() {
return 'Some sound';
}
}
module.exports = { Animal };
// Dog.js
const { Animal } = require('./base/Animal'); // ← import thẳng, không vòng tròn
class Dog extends Animal {
speak() {
return 'Woof';
}
}
module.exports = { Dog };
// Animal.js (giữ lại để re-export hoặc xử lý logic khác)
const { Animal } = require('./base/Animal');
const { Dog } = require('./Dog');
module.exports = { Animal, Dog };
Một nguyên tắc nên ghi nhớ: class cha không bao giờ nên import từ các class con. Nếu Animal.js cần tham chiếu đến Dog, đó là dấu hiệu thiết kế có vấn đề. Luồng phụ thuộc nên đi từ trên xuống, không quay ngược lại.
Cách khắc phục 2: Xóa import không cần thiết
Đôi khi file cha chỉ import một class con để re-export — không có mục đích nào khác. Hãy xem xét liệu import đó có thực sự cần thiết ở thời điểm tải hay không.
// Trước: Animal.js kéo Dog vào không vì lý do chức năng nào
const { Dog } = require('./Dog');
class Animal {}
module.exports = { Animal, Dog };
// Sau: nơi dùng import Dog trực tiếp
class Animal {}
module.exports = { Animal };
Cách khắc phục 3: Dời lời gọi require() vào trong hàm (chỉ áp dụng với CommonJS)
Đôi khi bạn thực sự cần cả hai class biết về nhau — chỉ là không cần ở thời điểm module được khởi tạo. Trong trường hợp đó, hãy chuyển require() vào trong thân hàm:
// Animal.js
class Animal {
createDog() {
const { Dog } = require('./Dog'); // ← chỉ chạy khi được gọi, không chạy lúc tải
return new Dog();
}
}
module.exports = { Animal };
Đến khi createDog() được gọi, cả hai module đã tải hoàn toàn và cache chứa đầy đủ các export. Tuy nhiên, đây chỉ là cách chữa triệu chứng. Nếu có thể tái cấu trúc lại code, hãy làm — deferred require làm các phụ thuộc bị rải rác khắp codebase và khiến việc refactor khó hơn.
Tự động tìm circular dependency
Đừng tự tay đọc file để tìm vòng tròn. madge làm điều đó trong vài giây:
npx madge --circular --extensions js src/
Dự án TypeScript:
npx madge --circular --extensions ts src/
Kết quả trông như sau:
Circular dependency found!
models/Dog.js → models/Animal.js → models/Dog.js
Tích hợp vào CI để vòng tròn không bao giờ lọt vào nữa:
npx madge --circular src/ && echo "No circular deps" || exit 1
Lưu ý về ESM (import/export)
ESM dùng live binding thay vì snapshot được cache, nên circular import hoạt động khác đi. Bạn vẫn có thể gặp đúng lỗi này — nếu một class được tham chiếu trước khi khai báo của nó được thực thi, bạn lại quay về undefined.
// Phiên bản ESM của cùng vấn đề
// animal.mjs
import { Dog } from './dog.mjs'; // dog.mjs import animal.mjs → vòng tròn
export class Animal {}
// Khắc phục: base.mjs không import gì từ các class con
export class Animal {}
Cùng nguyên nhân gốc rễ, cùng giải pháp.
Xác nhận đã khắc phục
- Chạy
npx madge --circular src/— xác nhận không còn cycle nào được báo. - Khởi động ứng dụng:
node index.jshoặcnpm start—TypeErrorphải biến mất. - Chạy bộ test:
npm test— các test khởi tạo class phải pass hết. - Người dùng Jest: nếu lỗi xuất hiện trong test, hãy xóa module cache trước bằng
jest --clearCache, rồi chạy lại.
Checklist nhanh
- File class cha có import từ class con không? Xóa nó đi.
- Hai file có dùng chung một kiểu dữ liệu hoặc interface? Tách ra file
types.jshoặc thư mụcbase/. - Đang dùng barrel file (
index.js) re-export tất cả? Barrel file dễ che giấu circular dep — hãy import trực tiếp từ file nguồn thay thế. madgeđã tìm ra vòng tròn chưa? Sửa mắt xích ngắn nhất trong chuỗi trước, rồi kiểm tra lại.

