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, namevà, 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ư 404hoặc 403hoặc 500.

JavaScript cho phép sử dụng throwvớ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, HttpTimeoutErrorcó thể kế thừa từ HttpError, v.v.

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ị jsoncó 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ínhnameage 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à SyntaxErrorvì 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ó ValidationErrorvà 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 ValidationErrortừ 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 namethà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: ValidationErrorSyntaxErrorđượ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à instanceofkiể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 catchgặ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 PropertyRequiredErrorrấ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.nametrong 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 MyErrorvà 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 readUsertrong đ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ó SyntaxErrorValidationError, nhưng trong tương lai hàm readUsercó 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 readUsersẽ xử lý các lỗi này. Ngay bây giờ, nó sử dụng nhiều ifs 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 readUsertạ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.

  1. 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.
  2. Hàm readUsersẽ bắt lỗi đọc dữ liệu xảy ra bên trong nó, chẳng hạn như ValidationErrorSyntaxError, và tạo ra một ReadErrorthay thế.
  3. Đối tượngReadError 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 readUsersẽ 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 ReadErrorvà thể hiện việc sử dụng nó trong readUsertry..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, readUserhoạ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 ReadErrorlỗ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 ReadErrorvà đó 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ừ Errorvà 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ính name và đừng quên gọi super.
  • 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.causetrong 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!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!