Khi chúng ta phát triển một cái gì đó, chúng ta thường cần các lớp lỗi của riêng mình để phản ánh những điều cụ thể có thể sai trong các nhiệm vụ của chúng ta. Đối với các lỗi trong hoạt động mạng, chúng ta có thể cần HttpError
, cho các hoạt động cơ sở dữ liệu DbError
, cho các hoạt động tìm kiếm NotFoundError
, v.v.
Các lỗi của chúng ta sẽ hỗ trợ các thuộc tính lỗi cơ bản như message
, name
và, tốt nhất là , stack
. Nhưng họ cũng có thể có các thuộc tính khác của riêng, ví dụ như đối tượngHttpError
có thể có một thuộc tínhstatusCode
với giá trị như 404
hoặc 403
hoặc 500
.
JavaScript cho phép sử dụng throw
với bất kỳ đối số nào, vì vậy về mặt kỹ thuật, các lớp tùy chỉnh lỗi của chúng ta không cần phải kế thừa từ Error
. Nhưng nếu chúng ta kế thừa, thì nó có thể được sử dụng obj instanceof Error
để xác định các đối tượng lỗi. Vì vậy, tốt hơn là thừa kế từ nó.
Khi ứng dụng phát triển, các lỗi của chúng ta tự nhiên tạo thành một hệ thống phân cấp. Chẳng hạn, HttpTimeoutError
có thể kế thừa từ HttpError
, v.v.
Nội dung chính
1. Mở rộng Lỗi
Ví dụ, hãy xem xét một hàm readUser(json)
nên đọc JSON với dữ liệu người dùng.
Đây là một ví dụ về cách một giá trị json
có thể trống:
let json = `{ "name": "John", "age": 30 }`;
Trong nội bộ, chúng ta sẽ sử dụng JSON.parse
. Nếu nó nhận được dị dạng json
, thì nó ném SyntaxError
. Nhưng ngay cả khi json
đúng về mặt cú pháp, điều đó không có nghĩa đó là người dùng hợp lệ, phải không? Nó có thể bỏ lỡ các dữ liệu cần thiết. Ví dụ, nó có thể không có các thuộc tínhname
và age
cần thiết cho người dùng của chúng ta.
Hàm của chúng ta readUser(json)
sẽ không chỉ đọc JSON, mà còn kiểm tra (xác thực tính xác thực) dữ liệu. Nếu không có trường bắt buộc hoặc định dạng sai, thì đó là lỗi. Và đó không phải là SyntaxError
vì dữ liệu đúng về mặt cú pháp, mà là một loại lỗi khác. Chúng tôi sẽ gọi nó ValidationError
và tạo một lớp cho nó. Một lỗi thuộc loại đó cũng sẽ mang thông tin về trường vi phạm.
LớpValidationError
của chúng ta nên kế thừa từ lớpError
.
Lớp đó được tích hợp sẵn, nhưng đây là code gần đúng của nó để chúng ta có thể hiểu những gì chúng ta đang mở rộng:
// The "pseudocode" for the built-in Error class defined by JavaScript itself
class Error {
constructor(message) {
this.message = message;
this.name = "Error"; // (different names for different built-in error classes)
this.stack = <call stack>; // non-standard, but most environments support it
}
}
Bây giờ hãy kế thừa ValidationError
từ nó và thử nó trong hành động:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
class ValidationError extends Error {
constructor(message) {
super(message); // (1)
this.name = "ValidationError"; // (2)
}
}
function test() {
throw new ValidationError("Whoops!");
}
try {
test();
} catch(err) {
alert(err.message); // Whoops!
alert(err.name); // ValidationError
alert(err.stack); // a list of nested calls with line numbers for each
}
Xin lưu ý: trong dòng (1)
chúng ta gọi hàm tạo cha. JavaScript yêu cầu chúng ta gọi super
trong hàm tạo con, vì vậy điều đó là bắt buộc. Các constructor cha đặt thuộc tínhmessage
.
Hàm tạo cha cũng đặt thuộc tính name
thành "Error"
, vì vậy trong dòng (2)
chúng ta đặt lại nó về đúng giá trị.
Hãy thử sử dụng nó trong readUser(json)
:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
// Usage
function readUser(json) {
let user = JSON.parse(json);
if (!user.age) {
throw new ValidationError("No field: age");
}
if (!user.name) {
throw new ValidationError("No field: name");
}
return user;
}
// Working example with try..catch
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No field: name
} else if (err instanceof SyntaxError) { // (*)
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // unknown error, rethrow it (**)
}
}
Các khốitry..catch
trong đoạn code trên cầm cả hai chúng: ValidationError
và SyntaxError
được xây dựng trong JSON.parse
.
Vui lòng xem cách chúng ta sử dụng instanceof
để kiểm tra loại lỗi cụ thể trong dòng (*)
.
Chúng ta cũng có thể nhìn vào err.name
, như thế này:
// ...
// instead of (err instanceof SyntaxError)
} else if (err.name == "SyntaxError") { // (*)
// ...
Các phiên bảninstanceof
là tốt hơn nhiều, bởi vì trong tương lai chúng ta sẽ mở rộng ValidationError
, làm cho phân nhóm của nó, giống như PropertyRequiredError
. Và instanceof
kiểm tra sẽ tiếp tục làm việc cho các lớp kế thừa mới. Vì vậy, đó là bằng chứng trong tương lai.
Ngoài ra, điều quan trọng là nếu catch
gặp một lỗi không xác định, thì nó sẽ lưu lại trong dòng (**)
. Các khối catch
chỉ biết làm thế nào để xử lý xác nhận và cú pháp sai sót, các loại khác (do một lỗi đánh máy trong các code hoặc những cái mà người khác chưa biết) nên rơi qua.
2. Kế thừa
Các lớpValidationError
là rất chung chung. Nhiều thứ có thể đi sai. Thuộc tính có thể vắng mặt hoặc nó có thể ở định dạng sai (như giá trị chuỗi cho age
). Chúng ta hãy tạo một lớp cụ thể hơn PropertyRequiredError
, chính xác cho các thuộc tính vắng mặt. Nó sẽ mang thêm thông tin về thuộc tính bị thiếu.
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.name = "PropertyRequiredError";
this.property = property;
}
}
// Usage
function readUser(json) {
let user = JSON.parse(json);
if (!user.age) {
throw new PropertyRequiredError("age");
}
if (!user.name) {
throw new PropertyRequiredError("name");
}
return user;
}
// Working example with try..catch
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Invalid data: " + err.message); // Invalid data: No property: name
alert(err.name); // PropertyRequiredError
alert(err.property); // name
} else if (err instanceof SyntaxError) {
alert("JSON Syntax Error: " + err.message);
} else {
throw err; // unknown error, rethrow it
}
}
Lớp mới PropertyRequiredError
rất dễ sử dụng: chúng ta chỉ cần truyền tên thuộc tính : new PropertyRequiredError(property)
. Con người có thể đọc được message
được tạo bởi các contructor.
Xin lưu ý rằng this.name
trong constructorPropertyRequiredError
một lần nữa được gán thủ công. Điều đó có thể trở nên hơi tẻ nhạt – để gán this.name = <class name>
trong mọi lớp tùy chỉnh lỗi. Chúng ta có thể tránh nó bằng cách tạo lớp lỗi lỗi cơ bản của riêng mình mà gán this.name = this.constructor.name
. Và sau đó kế thừa tất cả các lỗi tùy chỉnh của chúng ta từ nó.
Hãy gọi nó là MyError
.
Đây là code với MyError
và các lớp tùy chỉnh lỗi khác, được đơn giản hóa:
class MyError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
class ValidationError extends MyError { }
class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.property = property;
}
}
// name is correct
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
Bây giờ các tùy chỉnh lỗi ngắn hơn nhiều, đặc biệt ValidationError
, khi chúng ta thoát khỏi dòng"this.name = ..."
trong hàm tạo.
3. Bao bọc ngoại lệ
Mục đích của hàm readUser
trong đoạn code trên là có thể đọc dữ liệu người dùng. Có thể xảy ra các loại lỗi khác nhau trong quá trình. Ngay bây giờ chúng ta có SyntaxError
và ValidationError
, nhưng trong tương lai hàm readUser
có thể phát triển và có thể tạo ra các loại lỗi khác.
Code mà các lệnh gọi readUser
sẽ xử lý các lỗi này. Ngay bây giờ, nó sử dụng nhiều if
s trong khối catch
, kiểm tra lớp và xử lý các lỗi đã biết và suy nghĩ lại những lỗi chưa biết.
Đề án là như thế này:
try {
...
readUser() // the potential error source
...
} catch (err) {
if (err instanceof ValidationError) {
// handle validation errors
} else if (err instanceof SyntaxError) {
// handle syntax errors
} else {
throw err; // unknown error, rethrow it
}
}
Trong đoạn code trên chúng ta có thể thấy hai loại lỗi, nhưng có thể có nhiều hơn.
Nếu hàm readUser
tạo ra một số loại lỗi, thì chúng ta nên tự hỏi: chúng ta có thực sự muốn kiểm tra từng loại lỗi một lần không?
Thường thì câu trả lời là của Nô-lô-lô: chúng ta muốn được một cấp trên tất cả các cấp đó. Chúng ta chỉ muốn biết liệu có lỗi đọc dữ liệu của người dùng hay không – tại sao chính xác nó xảy ra thường không liên quan (thông báo lỗi mô tả nó). Hoặc, thậm chí tốt hơn, chúng ta muốn có một cách để có được các chi tiết lỗi, nhưng chỉ khi chúng ta cần.
Kỹ thuật mà chúng ta mô tả ở đây được gọi là ngoại lệ gói vụn.
- Chúng ta sẽ tạo một lớp mới
ReadError
để biểu thị một lỗi đọc dữ liệu chung chung. - Hàm
readUser
sẽ bắt lỗi đọc dữ liệu xảy ra bên trong nó, chẳng hạn nhưValidationError
vàSyntaxError
, và tạo ra mộtReadError
thay thế. - Đối tượng
ReadError
sẽ giữ tham chiếu đến lỗi ban đầu trong thuộc tính của nócause
.
Sau đó, code mà các lệnh gọi readUser
sẽ chỉ phải kiểm tra ReadError
, không phải cho mọi loại lỗi đọc dữ liệu. Và nếu nó cần thêm chi tiết về một lỗi, nó có thể kiểm tra thuộc tính của nó cause
.
Đây là code xác định ReadError
và thể hiện việc sử dụng nó trong readUser
và try..catch
:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
class ReadError extends Error {
constructor(message, cause) {
super(message);
this.cause = cause;
this.name = 'ReadError';
}
}
class ValidationError extends Error { /*...*/ }
class PropertyRequiredError extends ValidationError { /* ... */ }
function validateUser(user) {
if (!user.age) {
throw new PropertyRequiredError("age");
}
if (!user.name) {
throw new PropertyRequiredError("name");
}
}
function readUser(json) {
let user;
try {
user = JSON.parse(json);
} catch (err) {
if (err instanceof SyntaxError) {
throw new ReadError("Syntax Error", err);
} else {
throw err;
}
}
try {
validateUser(user);
} catch (err) {
if (err instanceof ValidationError) {
throw new ReadError("Validation Error", err);
} else {
throw err;
}
}
}
try {
readUser('{bad json}');
} catch (e) {
if (e instanceof ReadError) {
alert(e);
// Original error: SyntaxError: Unexpected token b in JSON at position 1
alert("Original error: " + e.cause);
} else {
throw e;
}
}
Trong đoạn code trên, readUser
hoạt động chính xác như được mô tả – nắm bắt lỗi cú pháp và xác nhận và ném ReadError
lỗi thay vào đó (lỗi không xác định được lưu lại như bình thường).
Vì vậy, code bên ngoài kiểm tra instanceof ReadError
và đó là nó. Không cần phải liệt kê tất cả các loại lỗi có thể.
Cách tiếp cận này được gọi là bao bọc ngoại lệ, bởi vì chúng ta sử dụng các trường hợp ngoại lệ ở cấp độ thấp và cách bao bọc của họ vào ReadError
đó là trừu tượng hơn. Nó được sử dụng rộng rãi trong lập trình hướng đối tượng.
4. Tóm lược
- Chúng ta có thể kế thừa từ
Error
và các lớp lỗi tích hợp khác một cách bình thường. Chúng ta chỉ cần quan tâm thuộc tínhname
và đừng quên gọisuper
. - Chúng ta có thể sử dụng
instanceof
để kiểm tra các lỗi cụ thể. Nó cũng hoạt động với sự kế thừa. Nhưng đôi khi chúng ta có một đối tượng lỗi đến từ thư viện của bên thứ 3 và không có cách nào dễ dàng để có được lớp của nó. Sau đó, thuộc tínhname
có thể được sử dụng để kiểm tra như vậy. - Bao bọc các ngoại lệ là một kỹ thuật phổ biến: một hàm xử lý các ngoại lệ cấp thấp và tạo ra các lỗi cấp cao hơn thay vì các lỗi cấp thấp khác nhau. Các ngoại lệ cấp thấp đôi khi trở thành thuộc tính của đối tượng đó như
err.cause
trong các ví dụ trên, nhưng điều đó không bắt buộc.
Full series tự học Javascript từ cơ bản tới nâng cao tại đây nha.
Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:
Chào thân ái và quyết thắng!