Javascript 自定義 Error,擴(kuò)展 Error

2023-02-17 10:53 更新

當(dāng)我們?cè)陂_發(fā)某些東西時(shí),經(jīng)常會(huì)需要我們自己的 error 類來(lái)反映在我們的任務(wù)中可能出錯(cuò)的特定任務(wù)。對(duì)于網(wǎng)絡(luò)操作中的 error,我們需要 ?HttpError?,對(duì)于數(shù)據(jù)庫(kù)操作中的 error,我們需要 ?DbError?,對(duì)于搜索操作中的 error,我們需要 ?NotFoundError?,等等。

我們自定義的 error 應(yīng)該支持基本的 error 的屬性,例如 message,name,并且最好還有 stack。但是它們也可能會(huì)有其他屬于它們自己的屬性,例如,HttpError 對(duì)象可能會(huì)有一個(gè) statusCode 屬性,屬性值可能為 404、403 或 500 等。

JavaScript 允許將 throw 與任何參數(shù)一起使用,所以從技術(shù)上講,我們自定義的 error 不需要從 Error 中繼承。但是,如果我們繼承,那么就可以使用 obj instanceof Error 來(lái)識(shí)別 error 對(duì)象。因此,最好繼承它。

隨著開發(fā)的應(yīng)用程序的增長(zhǎng),我們自己的 error 自然會(huì)形成形成一個(gè)層次結(jié)構(gòu)(hierarchy)。例如,HttpTimeoutError 可能繼承自 HttpError,等等。

擴(kuò)展 Error

例如,讓我們考慮一個(gè)函數(shù) readUser(json),該函數(shù)應(yīng)該讀取帶有用戶數(shù)據(jù)的 JSON。

這里是一個(gè)可用的 json 的例子:

let json = `{ "name": "John", "age": 30 }`;

在函數(shù)內(nèi)部,我們將使用 JSON.parse。如果它接收到格式不正確的 json,就會(huì)拋出 SyntaxError。但是,即使 json 在語(yǔ)法上是正確的,也不意味著該數(shù)據(jù)是有效的用戶數(shù)據(jù),對(duì)吧?因?yàn)樗赡軄G失了某些必要的數(shù)據(jù)。例如,對(duì)用戶來(lái)說(shuō),必不可少的是 name 和 age 屬性。

我們的函數(shù) readUser(json) 不僅會(huì)讀取 JSON,還會(huì)檢查(“驗(yàn)證”)數(shù)據(jù)。如果沒(méi)有所必須的字段,或者(字段的)格式錯(cuò)誤,那么就會(huì)出現(xiàn)一個(gè) error。并且這些并不是 SyntaxError,因?yàn)檫@些數(shù)據(jù)在語(yǔ)法上是正確的,這些是另一種錯(cuò)誤。我們稱之為 ValidationError,并為之創(chuàng)建一個(gè)類。這種類型的錯(cuò)誤也應(yīng)該包含有關(guān)違規(guī)字段的信息。

我們的 ValidationError 類應(yīng)該繼承自 Error 類。

Error 類是內(nèi)建的,但這是其近似代碼,所以我們可以了解我們要擴(kuò)展的內(nèi)容:

// JavaScript 自身定義的內(nèi)建的 Error 類的“偽代碼”
class Error {
  constructor(message) {
    this.message = message;
    this.name = "Error"; // (不同的內(nèi)建 error 類有不同的名字)
    this.stack = <call stack>; // 非標(biāo)準(zhǔn)的,但大多數(shù)環(huán)境都支持它
  }
}

現(xiàn)在讓我們從其中繼承 ValidationError,并嘗試進(jìn)行運(yùn)行:

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); // 一個(gè)嵌套調(diào)用的列表,每個(gè)調(diào)用都有對(duì)應(yīng)的行號(hào)
}

請(qǐng)注意:在 (1) 行中我們調(diào)用了父類的 constructor。JavaScript 要求我們?cè)谧宇惖?constructor 中調(diào)用 super,所以這是必須的。父類的 constructor 設(shè)置了 message 屬性。

父類的 constructor 還將 name 屬性的值設(shè)置為了 "Error",所以在 (2) 行中,我們將其重置為了右邊的值。

讓我們嘗試在 readUser(json) 中使用它吧:

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

// 用法
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;
}

// 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; // 未知的 error,再次拋出 (**)
  }
}

上面代碼中的 try..catch 塊既處理我們的 ValidationError 又處理來(lái)自 JSON.parse 的內(nèi)建 SyntaxError。

請(qǐng)看一下我們是如何使用 instanceof 來(lái)檢查 (*) 行中的特定錯(cuò)誤類型的。

我們也可以看看 err.name,像這樣:

// ...
// instead of (err instanceof SyntaxError)
} else if (err.name == "SyntaxError") { // (*)
// ...

使用 instanceof 的版本要好得多,因?yàn)閷?lái)我們會(huì)對(duì) ValidationError 進(jìn)行擴(kuò)展,創(chuàng)建它的子類型,例如 PropertyRequiredError。而 instanceof 檢查對(duì)于新的繼承類也適用。所以這是面向未來(lái)的做法。

還有一點(diǎn)很重要,在 catch 遇到了未知的錯(cuò)誤,它會(huì)在 (**) 行將該錯(cuò)誤再次拋出。catch 塊只知道如何處理 validation 錯(cuò)誤和語(yǔ)法錯(cuò)誤,而其他錯(cuò)誤(由代碼中的拼寫錯(cuò)誤或其他未知原因?qū)е碌模?yīng)該被扔出(fall through)。

深入繼承

ValidationError 類是非常通用的。很多東西都可能出錯(cuò)。對(duì)象的屬性可能缺失或者屬性可能有格式錯(cuò)誤(例如 age 屬性的值為一個(gè)字符串而不是數(shù)字)。讓我們針對(duì)缺少屬性的錯(cuò)誤來(lái)制作一個(gè)更具體的 PropertyRequiredError 類。它將攜帶有關(guān)缺少的屬性的相關(guān)信息。

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;
  }
}

// 用法
function readUser(json) {
  let user = JSON.parse(json);

  if (!user.age) {
    throw new PropertyRequiredError("age");
  }
  if (!user.name) {
    throw new PropertyRequiredError("name");
  }

  return user;
}

// 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; // 未知 error,將其再次拋出
  }
}

這個(gè)新的類 PropertyRequiredError 使用起來(lái)很簡(jiǎn)單:我們只需要傳遞屬性名:new PropertyRequiredError(property)。人類可讀的 message 是由 constructor 生成的。

請(qǐng)注意,在 PropertyRequiredError constructor 中的 this.name 是通過(guò)手動(dòng)重新賦值的。這可能會(huì)變得有些乏味 —— 在每個(gè)自定義 error 類中都要進(jìn)行 this.name = <class name> 賦值操作。我們可以通過(guò)創(chuàng)建自己的“基礎(chǔ)錯(cuò)誤(basic error)”類來(lái)避免這種情況,該類進(jìn)行了 this.name = this.constructor.name 賦值。然后讓所有我們自定義的 error 都從這個(gè)“基礎(chǔ)錯(cuò)誤”類進(jìn)行繼承。

讓我們稱之為 MyError。

這是帶有 MyError 以及其他自定義的 error 類的代碼,已進(jìn)行簡(jiǎn)化:

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 是對(duì)的
alert( new PropertyRequiredError("field").name ); // PropertyRequiredError

現(xiàn)在自定義的 error 短了很多,特別是 ValidationError,因?yàn)槲覀償[脫了 constructor 中的 "this.name = ..." 這一行。

包裝異常

在上面代碼中的函數(shù) readUser 的目的就是“讀取用戶數(shù)據(jù)”。在這個(gè)過(guò)程中可能會(huì)出現(xiàn)不同類型的 error。目前我們有了 SyntaxError 和 ValidationError,但是將來(lái),函數(shù) readUser 可能會(huì)不斷壯大,并可能會(huì)產(chǎn)生其他類型的 error。

調(diào)用 readUser 的代碼應(yīng)該處理這些 error。現(xiàn)在它在 catch 塊中使用了多個(gè) if 語(yǔ)句來(lái)檢查 error 類,處理已知的 error,并再次拋出未知的 error。

該方案是這樣的:

try {
  ...
  readUser()  // 潛在的 error 源
  ...
} catch (err) {
  if (err instanceof ValidationError) {
    // 處理 validation error
  } else if (err instanceof SyntaxError) {
    // 處理 syntax error
  } else {
    throw err; // 未知 error,再次拋出它
  }
}

在上面的代碼中,我們可以看到兩種類型的 error,但是可以有更多。

如果 readUser 函數(shù)會(huì)產(chǎn)生多種 error,那么我們應(yīng)該問(wèn)問(wèn)自己:我們是否真的想每次都一一檢查所有的 error 類型?

通常答案是 “No”:我們希望能夠“比它高一個(gè)級(jí)別”。我們只想知道這里是否是“數(shù)據(jù)讀取異常” —— 為什么發(fā)生了這樣的 error 通常是無(wú)關(guān)緊要的(error 信息描述了它)?;蛘?,如果我們有一種方式能夠獲取 error 的詳細(xì)信息那就更好了,但前提是我們需要。

我們所描述的這項(xiàng)技術(shù)被稱為“包裝異常”。

  1. 我們將創(chuàng)建一個(gè)新的類 ?ReadError? 來(lái)表示一般的“數(shù)據(jù)讀取” error。
  2. 函數(shù)readUser 將捕獲內(nèi)部發(fā)生的數(shù)據(jù)讀取 error,例如 ?ValidationError? 和 ?SyntaxError?,并生成一個(gè) ?ReadError? 來(lái)進(jìn)行替代。
  3. 對(duì)象 ?ReadError? 會(huì)把對(duì)原始 error 的引用保存在其 ?cause? 屬性中。

之后,調(diào)用 readUser 的代碼只需要檢查 ReadError,而不必檢查每種數(shù)據(jù)讀取 error。并且,如果需要更多 error 細(xì)節(jié),那么可以檢查 readUser 的 cause 屬性。

下面的代碼定義了 ReadError,并在 readUser 和 try..catch 中演示了其用法:

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;
  }
}

在上面的代碼中,readUser 正如所描述的那樣正常工作 —— 捕獲語(yǔ)法和驗(yàn)證(validation)錯(cuò)誤,并拋出 ReadError(對(duì)于未知錯(cuò)誤將照常再次拋出)。

所以外部代碼檢查 instanceof ReadError,并且它的確是。不必列出所有可能的 error 類型。

這種方法被稱為“包裝異常(wrapping exceptions)”,因?yàn)槲覀儗ⅰ暗图?jí)別”的異?!鞍b”到了更抽象的 ReadError 中。它被廣泛應(yīng)用于面向?qū)ο蟮木幊讨小?

總結(jié)

  • 我們可以正常地從 ?Error? 和其他內(nèi)建的 error 類中進(jìn)行繼承,。我們只需要注意 ?name? 屬性以及不要忘了調(diào)用 ?super?。
  • 我們可以使用 ?instanceof? 來(lái)檢查特定的 error。但有時(shí)我們有來(lái)自第三方庫(kù)的 error 對(duì)象,并且在這兒沒(méi)有簡(jiǎn)單的方法來(lái)獲取它的類。那么可以將 ?name? 屬性用于這一類的檢查。
  • 包裝異常是一項(xiàng)廣泛應(yīng)用的技術(shù):用于處理低級(jí)別異常并創(chuàng)建高級(jí)別 error 而不是各種低級(jí)別 error 的函數(shù)。在上面的示例中,低級(jí)別異常有時(shí)會(huì)成為該對(duì)象的屬性,例如 ?err.cause?,但這不是嚴(yán)格要求的。

任務(wù)


繼承 SyntaxError

重要程度: 5

創(chuàng)建一個(gè)繼承自內(nèi)建類 SyntaxError 的類 FormatError。

它應(yīng)該支持 message,name 和 stack 屬性。

用例:

let err = new FormatError("formatting error");

alert( err.message ); // formatting error
alert( err.name ); // FormatError
alert( err.stack ); // stack

alert( err instanceof FormatError ); // true
alert( err instanceof SyntaxError ); // true(因?yàn)樗^承自 SyntaxError)

解決方案

class FormatError extends SyntaxError {
  constructor(message) {
    super(message);
    this.name = this.constructor.name;
  }
}

let err = new FormatError("formatting error");

alert( err.message ); // formatting error
alert( err.name ); // FormatError
alert( err.stack ); // stack

alert( err instanceof SyntaxError ); // true


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)