Câu 1Ngoại lệ tạo từ class ShippingError extends Error {} có name là gì?
Ném ngoại lệ — throw và lỗi tùy chỉnh
Ném ngoại lệ bằng throw new Error, tự tạo loại lỗi riêng bằng extends Error, và giữ lại lỗi gốc bằng cause.
Kể cả khi trang đặt hàng gửi lên số lượng 0, hàm tính tiền tạm tính vẫn cứ thế tính, và một đơn hàng có tổng tiền bằng 0 được tạo ra. Chỉ hiển thị cảnh báo thì cũng không ngăn được phía gọi: nó không nhận ra có vấn đề và vẫn tiếp tục lưu đơn hàng.
Bài này nói về throw new Error để bạn tự ném ra ngoại lệ, và lỗi tùy chỉnh (custom error) là những loại lỗi do chính bạn định nghĩa.
Dừng lại khi gặp giá trị sai — throw new Error
Giả sử bạn muốn dừng lại trước khi tính tạm tính nếu số lượng là số thập phân hoặc số âm. Báo hiệu bằng return null là chưa đủ: nếu phía gọi quên kiểm tra, phép cộng phí vận chuyển null + 500 sẽ cho ra 500, và việc tính toán vẫn tiếp tục.
Error object (giá trị có name và message, dùng để ném ra như một ngoại lệ) được tạo bằng new Error("nội dung"). Khi truyền nó cho throw, ngoại lệ xảy ra ngay tại chỗ; nếu lời gọi nằm trong try, giá trị đó sẽ tới catch dưới dạng error.
function calcSubtotal(unitPrice, quantity) {
// Nếu không phải số nguyên từ 1 trở lên, ném ngoại lệ ngay tại đây
if (!Number.isInteger(quantity) || quantity < 1) {
// Chỉ new Error thì chưa dừng được gì. Phải truyền cho throw để thành ngoại lệ
throw new Error(`Số lượng phải là số nguyên từ 1 trở lên: ${quantity}`);
}
return unitPrice * quantity;
}
try {
console.log(calcSubtotal(1200, 3)); // 3600
console.log(calcSubtotal(1200, 0)); // ném ngoại lệ nên không in ra gì
console.log("Đã xác nhận đơn hàng"); // dòng này cũng không chạy
} catch (error) {
console.log(error.name); // Error
console.log(error.message); // Số lượng phải là số nguyên từ 1 trở lên: 0
}
Hãy đặt câu if kiểm tra ở đầu hàm, trước phần tính toán. throw thoát khỏi hàm ngay tại chỗ giống như return, nên return unitPrice * quantity; phía sau không được chạy, và phía gọi cũng không bao giờ nhận về 0.
Tạo loại lỗi cho dữ liệu nhập sai — extends Error
Ngoại lệ ném bằng new Error luôn có name là Error, dù nội dung kiểm tra là gì. Mã bưu chính sai số chữ số hay sản phẩm hết hàng đều tới với cùng một tên, nên khối catch không phân biệt được đây có phải là lỗi mà người dùng tự sửa được bằng cách nhập lại hay không.
Khi thêm extends Error vào một class (cú pháp định nghĩa loại object được tạo bằng new), bạn tạo được một loại kế thừa cách hoạt động của Error. Nếu viết name = "ValidationError"; bên trong cặp ngoặc nhọn, các ngoại lệ tạo từ loại này sẽ có name là chuỗi đó.
// Kế thừa Error để tạo loại lỗi cho dữ liệu nhập sai
class ValidationError extends Error {
name = "ValidationError"; // trở thành name của ngoại lệ tạo bằng new
}
function checkZipCode(zipCode) {
if (zipCode.length !== 5) {
throw new ValidationError(`Hãy nhập mã bưu chính 5 chữ số: ${zipCode}`);
}
return zipCode;
}
try {
checkZipCode("9021");
} catch (error) {
console.log(error.name); // ValidationError
console.log(error.message); // Hãy nhập mã bưu chính 5 chữ số: 9021
console.log(error instanceof Error); // true
}
- Với ngoại lệ tạo bằng
new Error("nội dung")và với mọi loại trong khung này,instanceof Errorđều làtrue
SyntaxError/TypeError/RangeError— các loại đã gặp ở bài trước
ValidationError— biểu thị dữ liệu nhập sainamelà"ValidationError"đã viết trong class- instanceof ValidationError chỉ là true trong khung này
Bạn cũng có thể truyền một chuỗi cho throw, nhưng chuỗi không có name lẫn message, nên đọc error.message trong catch sẽ ra undefined. Bảng dưới đây tóm tắt những gì đọc được trong catch với từng loại giá trị truyền cho throw.
| Giá trị truyền cho throw | name đọc được trong catch | instanceof Error |
|---|---|---|
| "Mã bưu chính sai" (chuỗi) | undefined (chuỗi không có name) | false |
| new Error("Mã bưu chính sai") | Error | true |
| ValidationError có viết name | ValidationError | true |
| ValidationError không viết name | Error (giữ nguyên tên kế thừa) | true |
Kiểm tra loại tự tạo trước — thứ tự của instanceof
Nếu là lỗi nhập liệu thì người dùng có thể tự sửa, còn một ngoại lệ ngoài dự kiến như TypeError thì nhập lại thế nào cũng không hết. Bạn muốn catch hiển thị hai thông báo khác nhau, nhưng nếu viết các điều kiện sai thứ tự, ngay cả lỗi nhập liệu cũng nhận thông báo dành cho ngoại lệ ngoài dự kiến.
if và else if được kiểm tra từ trên xuống, và chỉ nhánh khớp đầu tiên được chạy. Một ValidationError cũng cho true với instanceof Error, nên nếu instanceof Error đứng trước, ValidationError cũng sẽ rơi vào nhánh đó.
class ValidationError extends Error {
name = "ValidationError";
}
const error = new ValidationError("Hãy nhập mã bưu chính 5 chữ số");
// Kiểm tra Error trước thì ValidationError cũng rơi vào nhánh này
if (error instanceof Error) {
console.log("Không lưu được"); // Không lưu được
} else if (error instanceof ValidationError) {
console.log("Hãy kiểm tra dữ liệu đã nhập"); // không đi vào đây
}
// Kiểm tra loại kế thừa trước
if (error instanceof ValidationError) {
console.log("Hãy kiểm tra dữ liệu đã nhập"); // Hãy kiểm tra dữ liệu đã nhập
} else if (error instanceof Error) {
console.log("Không lưu được");
}
Các loại có sẵn như TypeError cũng kế thừa Error, nên nhánh instanceof Error đặt ở cuối sẽ gom được tất cả. Hãy dùng nhánh đó để hiển thị một thông báo khác cho những ngoại lệ mà nhập lại cũng không giải quyết được.
Ném kèm ngoại lệ gốc — cause và ngoại lệ không được bắt
Giả sử một SyntaxError xảy ra trong hàm đọc phản hồi của dịch vụ thanh toán. Nếu chỉ ném lại nguyên như vậy, loại lỗi không cho phía gọi biết rằng lỗi xảy ra trong lúc xử lý thanh toán. Nhưng nếu thay bằng một ngoại lệ thuộc loại mới, bạn lại mất đi thông tin rằng ban đầu nó là SyntaxError.
cause (tùy chọn gắn ngoại lệ gốc gây ra lỗi vào ngoại lệ mới) được viết ở đối số thứ hai, như new Error("nội dung", { cause: error }). Với các loại tạo bằng extends Error cũng vậy, và phía bắt được ngoại lệ mới có thể đọc ngoại lệ gốc từ error.cause.
class PaymentError extends Error {
name = "PaymentError";
}
function readPayment(text) {
try {
return JSON.parse(text);
} catch (error) {
// Gắn ngoại lệ gốc vào cause rồi ném ra dưới loại lỗi thanh toán
throw new PaymentError("Không đọc được kết quả thanh toán", { cause: error });
}
}
try {
readPayment('{"orderId":"P-3107","amount":}');
} catch (error) {
console.log(error.name); // PaymentError
console.log(error.message); // Không đọc được kết quả thanh toán
console.log(error.cause.name); // SyntaxError
}
error.namelàPaymentErrorerror.messagelàKhông đọc được kết quả thanh toán
error.cause.namelàSyntaxErrorerror.cause.messagegiữ nguyên phần mô tả do JSON.parse đưa ra
Nếu thay bằng ngoại lệ mới mà không có cause, error.cause sẽ là undefined, và bạn không đọc được loại lẫn phần mô tả ban đầu. Có cause thì phía gọi vừa dùng error.name để hiển thị thông báo lỗi thanh toán, vừa ghi log được nội dung trong error.cause.
Ngoại lệ không ai bắt sẽ làm dừng chương trình
Nếu không có catch nào nhận ngoại lệ đã ném, nó trở thành ngoại lệ không được bắt (uncaught exception — ngoại lệ không được catch nào xử lý), và code đang chạy dừng ngay tại đó; không dòng nào còn lại được chạy. Nếu bỏ try bên ngoài trong đoạn code phía trên, bạn sẽ thấy một lỗi hiển thị tên loại và nội dung, như PaymentError: Không đọc được kết quả thanh toán.
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Nếu kiểm tra instanceof Error trước, ngoại lệ ValidationError sẽ đi vào nhánh nào?
Câu 3Bạn đã bắt bằng catch (error) một ngoại lệ được ném kèm cause. Làm sao để đọc name của ngoại lệ gốc?