Học bằng cách đọc theo thứ tự

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ó namemessage, 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
}
Code chỉ dừng khi được truyền cho throw
Trong if chỉ cónew ErrorMột Error objectđược tạo raKhông có gì xảyra, chạy dòng sau1200 * 0trả về 0Trong if cóthrow new ErrorMột Error objectđược tạo raNgoại lệ xảy rangay tại chỗGiá trị tới errorcủa catch
Cả hai cách viết đều tạo ra Error object. Chỉ khi truyền nó cho throw thì code mới dừng ngay tại chỗ.

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.

Kiểm tra mức hài lòng trước khi lưu một bài khảo sát. scores đã được khai báo sẵn.

① Nếu mức hài lòng không phải số nguyên, hãy ném ngoại lệ với nội dung "Mức hài lòng phải là số nguyên".

② Nếu nằm ngoài khoảng 1–5, hãy ném ngoại lệ với nội dung "Mức hài lòng phải từ 1 đến 5".

③ Nếu không có vấn đề gì, hãy trả về theo dạng "Đã gửi với mức hài lòng 4".

④ Hãy lần lượt truyền từng giá trị trong scores, vẫn chạy tiếp dù có giá trị bị lỗi, và hiển thị kết quả hoặc nội dung lỗi.

(Nếu chạy đúng, phần giải thích sẽ xuất hiện.)

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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ó nameError, 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ị trí của các loại tạo bằng extends Error
Error — các loại ngoại lệ có message và name
  • 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
Loại có sẵn — JavaScript cung cấp từ đầu
  • SyntaxError / TypeError / RangeError — các loại đã gặp ở bài trước
Các loại tạo bằng extends Error
  • ValidationError — biểu thị dữ liệu nhập sai
  • name"ValidationError" đã viết trong class
  • instanceof ValidationError chỉ là true trong khung này
Cả hai đều nằm trong khung Error, và không cái nào nằm trong khung của cái kia. instanceof ValidationError chỉ là true trong khung của chính nó.

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 throwname đọc được trong catchinstanceof 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")Errortrue
ValidationError có viết nameValidationErrortrue
ValidationError không viết nameError (giữ nguyên tên kế thừa)true

Hiển thị lý do một file bị từ chối. QuotaError, filescheckFile đã được khai báo sẵn.

① Hãy định nghĩa FormatError kế thừa Error và đặt name cho nó.

② Trong checkFile, nếu phần mở rộng không phải pdf, hãy ném FormatError với nội dung "Chỉ nhận file PDF".

③ Hãy kiểm tra lần lượt từng file trong files và hiển thị giá trị trả về hoặc "name: message".

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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.

ifelse 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");
}
Thứ tự kiểm tra quyết định nhánh được chạy
instanceof Errorviết trướcValidationErrorcũng là trueĐiều kiện saukhông được thửIn ra "Khônglưu được"ValidationErrorviết trướctrue vì tạo từValidationErrorĐiều kiện Errorkhông được thửIn ra lời nhắckiểm tra dữ liệu
Ở hàng trên, điều kiện thứ nhất là true nên điều kiện thứ hai không bao giờ được thử. Hãy kiểm tra các loại kế thừa Error trước chính Error.

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.

Xử lý các yêu cầu đổi địa chỉ email. ValidationError, changeEmailrequests đã được khai báo sẵn.

① Hãy truyền từng phần tử trong requests cho changeEmail và hiển thị kết quả.

② Với ValidationError, hãy hiển thị "Hãy kiểm tra địa chỉ email: " kèm nội dung lỗi.

③ Với các Error khác, hãy hiển thị "Không đổi được: " kèm tên loại lỗi.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra

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
}
Ngoại lệ gốc được giữ trong cause
error — PaymentError do readPayment ném ra
  • error.namePaymentError
  • error.messageKhông đọc được kết quả thanh toán
error.cause — ngoại lệ gốc do JSON.parse ném ra
  • error.cause.nameSyntaxError
  • error.cause.message giữ nguyên phần mô tả do JSON.parse đưa ra
Ngoại lệ bên ngoài là PaymentError, còn cause chứa SyntaxError gốc. Ngoại lệ gốc được giữ nguyên vẹn trong error.cause.

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.

Nhập dữ liệu chấm công. ImportErrorrecords đã được khai báo sẵn.

① Trong importRecords, hãy lấy 5 ký tự đầu của từng giờ vào làm và hiển thị theo dạng "E-104: 09:02".

② Nếu không lấy được, hãy ném ImportError với nội dung "Đã dừng nhập tại dòng của E-105", kèm theo ngoại lệ gốc.

③ Hãy gọi importRecords, và nếu thất bại thì hiển thị nội dung lỗi, rồi hiển thị "Ngoại lệ gốc: " kèm tên loại của ngoại lệ gốc.

JavaScript / TypeScript Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Ngoại lệ tạo từ class ShippingError extends Error {}name là gì?

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?