Custom error class: phân loại lỗi để xử lý và phản hồi nhất quán
Ném throw new Error('something went wrong') khắp nơi khiến tầng trên không cách nào phân biệt lỗi nào đáng retry, lỗi nào trả 400, lỗi nào trả 500. Custom error class giải quyết bằng cách gắn kiểu và metadata (mã lỗi, HTTP status, ngữ cảnh) vào lỗi, để code xử lý quyết định dựa trên kiểu thay vì so chuỗi message. Đây là nền của một lớp xử lý lỗi nhất quán: một error middleware nhìn kiểu lỗi và tự biết trả status nào, log mức nào, có lộ chi tiết cho client hay không.
Cơ chế hoạt động
Custom error kế thừa Error và thêm field riêng. Một base class chung gom phần lặp lại (gắn name, giữ stack trace), các lớp con khai báo ngữ nghĩa cụ thể:
class AppError extends Error {
constructor(message, { status = 500, code = 'INTERNAL', cause } = {}) {
super(message, { cause })
this.name = this.constructor.name // để stack/log hiện đúng tên lớp
this.status = status
this.code = code
Error.captureStackTrace?.(this, this.constructor) // bỏ frame constructor khỏi trace
}
}
class ValidationError extends AppError {
constructor(message, fields) { super(message, { status: 400, code: 'VALIDATION' }); this.fields = fields }
}
class NotFoundError extends AppError {
constructor(resource) { super(`${resource} không tồn tại`, { status: 404, code: 'NOT_FOUND' }) }
}
Error.captureStackTrace (V8/Node) cắt frame của constructor ra khỏi stack, để trace trỏ tới nơi ném lỗi chứ không phải nội bộ class. Đặt this.name = this.constructor.name để log và instanceof hoạt động đúng.
Vấn đề gặp trong production
Failure mode: phản hồi lỗi không nhất quán. Khi lỗi chỉ là chuỗi, mỗi nơi tự chế format response, client nhận lúc thì { error: "..." }, lúc thì { message: "..." }, lúc thì 500 cho cả lỗi validation. Với error class, một chỗ duy nhất chuyển lỗi thành response theo kiểu:
app.use((err, req, res, next) => {
if (err instanceof AppError) {
return res.status(err.status).json({ code: err.code, message: err.message, fields: err.fields })
}
logger.error(err) // lỗi không lường trước -> log full, trả 500 chung chung
res.status(500).json({ code: 'INTERNAL', message: 'Internal error' })
})
Lỗi đã biết (validation, not found) trả message rõ cho client; lỗi không lường trước trả 500 chung và không lộ chi tiết nội bộ — quan trọng về bảo mật, vì message lỗi gốc có thể chứa query, đường dẫn, hoặc thông tin hệ thống.
Failure mode: dùng chuỗi message để rẽ nhánh. So sánh if (err.message === 'not found') cực kỳ giòn — đổi một chữ trong message là vỡ logic, và không dịch được message sang ngôn ngữ khác. Rẽ nhánh phải dựa trên instanceof hoặc err.code, không phải err.message. message dành cho con người đọc, code dành cho máy xử lý.
Một lưu ý khi transpile: kế thừa Error với target ES5 cũ có thể làm instanceof không hoạt động (do hạn chế của Object.setPrototypeOf thời đó). Với target hiện đại (ES2015+) thì ổn; nếu buộc target cũ, thêm Object.setPrototypeOf(this, new.target.prototype) trong constructor.
Cách debug và monitor
Phân loại lỗi cho phép log và alert theo mức đúng: lỗi nghiệp vụ đã biết (validation, not found) log mức info/warn và không cần alert; lỗi không lường (lọt vào nhánh 500) log error và bắn alert. Nếu log đang ngập warning từ validation lỗi của người dùng, đó là dấu hiệu phân loại sai mức. Gắn code ổn định vào mỗi loại lỗi để dashboard nhóm và đếm theo code — đột biến một code chỉ thẳng vào loại lỗi đang tăng. Luôn log err.stack và err.cause cho lỗi 500; với lỗi nghiệp vụ thì stack ít quan trọng hơn code + ngữ cảnh.
Tradeoff
Typed error (custom class) rõ ràng hơn hẳn chuỗi: rẽ nhánh an toàn theo instanceof/code, response nhất quán, log phân mức đúng — đổi lại phải định nghĩa và bảo trì một cây class. Với dự án nhỏ, một AppError với field code đã đủ; cây class sâu chỉ đáng khi thật sự có nhiều loại lỗi cần xử lý khác nhau. Quy tắc thực tế: tối thiểu một base AppError mang status + code, vài lớp con cho các loại lỗi nghiệp vụ thường gặp, một error middleware duy nhất chuyển thành response, và không bao giờ rẽ nhánh logic theo err.message.
Câu hỏi phỏng vấn
Tại sao cần custom error class thay vì
throw new Error('...'), và phân loại lỗi giúp gì ở tầng response?
Custom error class gắn kiểu và metadata (mã code, HTTP status, ngữ cảnh) vào lỗi, cho phép tầng trên rẽ nhánh dựa trên kiểu lỗi (instanceof/err.code) thay vì so chuỗi message vốn rất giòn và không quốc tế hóa được. Ở tầng response, một error middleware duy nhất nhìn kiểu lỗi và tự quyết: lỗi nghiệp vụ đã biết (validation → 400, not found → 404) trả message rõ và code ổn định cho client; lỗi không lường trước trả 500 chung và không lộ chi tiết nội bộ (tránh rò thông tin hệ thống). Điểm ăn điểm: dùng Error.captureStackTrace để trace trỏ đúng nơi ném, this.name = this.constructor.name cho log/instanceof đúng, phân mức log/alert theo loại lỗi, và giữ { cause } để không mất nguyên nhân gốc.
Hands-on
Xây một cây error nhỏ cho một API thật: AppError (base, mang status/code/cause) và các lớp con ValidationError, NotFoundError, UnauthorizedError. Viết một error middleware Express chuyển chúng thành response nhất quán và trả 500 chung cho lỗi lạ, rồi kiểm tra rằng một lỗi validation ra 400 với fields, một lỗi DB bất ngờ ra 500 không lộ chi tiết. Cố tình ném một Error thường ở một route để xác nhận nó rơi vào nhánh 500 và được log full stack, rồi đối chiếu log: lỗi nghiệp vụ ở mức warn không alert, lỗi 500 ở mức error có alert.
Top comments (0)