當我們開發某些東西時,我們經常需要自己的錯誤類別來反映任務中可能出錯的特定事項。對於網路操作中的錯誤,我們可能需要 HttpError
,對於資料庫操作,我們可能需要 DbError
,對於搜尋操作,我們可能需要 NotFoundError
,依此類推。
我們的錯誤應支援基本錯誤屬性,例如 message
、name
,最好還有 stack
。但它們也可能具有自己的其他屬性,例如 HttpError
物件可能有 statusCode
屬性,其值為 404
或 403
或 500
。
JavaScript 允許對任何引數使用 throw
,因此技術上我們的自訂錯誤類別不需要繼承自 Error
。但如果我們繼承,則可以使用 obj instanceof Error
來識別錯誤物件。因此最好繼承自它。
隨著應用程式的發展,我們自己的錯誤自然會形成一個層級。例如,HttpTimeoutError
可能繼承自 HttpError
,依此類推。
擴充 Error
舉例來說,我們考慮一個函式 readUser(json)
,它應該讀取包含使用者資料的 JSON。
以下是有效的 json
範例
let json = `{ "name": "John", "age": 30 }`;
在內部,我們將使用 JSON.parse
。如果它收到格式錯誤的 json
,它會擲出 SyntaxError
。但即使 json
在語法上正確,也不代表它是一個有效的使用者,對吧?它可能會遺漏必要的資料。例如,它可能沒有我們使用者必要的 name
和 age
屬性。
我們的函式 readUser(json)
不僅會讀取 JSON,還會檢查(「驗證」)資料。如果沒有必要欄位,或格式錯誤,那是一個錯誤。那不是 SyntaxError
,因為資料在語法上正確,而是另一種錯誤。我們將稱之為 ValidationError
,並為它建立一個類別。這種錯誤也應該載有違規欄位的資訊。
我們的 ValidationError
類別應該繼承自 Error
類別。
Error
類別是內建的,但以下是它的近似程式碼,讓我們可以了解我們正在擴充什麼
// 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
}
}
現在讓我們從它繼承 ValidationError
並在實際情況中試用它
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
}
請注意:在第 (1)
行中,我們呼叫父建構函式。JavaScript 要求我們在子建構函式中呼叫 super
,因此這是必要的。父建構函式設定 message
屬性。
父建構函式也會將 name
屬性設定為 "Error"
,因此在第 (2)
行中,我們將它重設為正確的值。
讓我們在 readUser(json)
中試用它
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 (**)
}
}
上述程式碼中的 try..catch
區塊處理我們自己的 ValidationError
和來自 JSON.parse
的內建 SyntaxError
。
請看我們如何在第 (*)
行中使用 instanceof
檢查特定錯誤類型。
我們也可以查看 err.name
,如下所示
// ...
// instead of (err instanceof SyntaxError)
} else if (err.name == "SyntaxError") { // (*)
// ...
instanceof
版本好得多,因為未來我們將擴充 ValidationError
,建立它的子類型,例如 PropertyRequiredError
。而 instanceof
檢查將繼續適用於新的繼承類別。因此這是具有未來性的。
此外,如果 catch
遇見未知錯誤,它會在第 (**)
行中重新擲出它,這一點也很重要。catch
區塊只知道如何處理驗證和語法錯誤,其他種類(由程式碼中的錯字或其他未知原因造成)應該會穿透。
進一步繼承
ValidationError
類別非常通用。許多事情可能會出錯。屬性可能不存在,或者格式錯誤(例如 age
的字串值而不是數字)。讓我們建立一個更具體的類別 PropertyRequiredError
,專門針對不存在的屬性。它將攜帶有關遺失屬性的附加資訊。
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
}
}
新的類別 PropertyRequiredError
很容易使用:我們只需要傳遞屬性名稱:new PropertyRequiredError(property)
。人類可讀的 message
由建構函式產生。
請注意,PropertyRequiredError
建構函式中的 this.name
再次手動指定。這可能會變得有點繁瑣,在每個自訂錯誤類別中指定 this.name = <class name>
。我們可以透過建立自己的「基本錯誤」類別來避免它,該類別指定 this.name = this.constructor.name
。然後從中繼承我們所有自訂錯誤。
我們稱它為 MyError
。
以下是使用 MyError
和其他自訂錯誤類別的程式碼,簡化版
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
現在自訂錯誤短很多,特別是 ValidationError
,因為我們在建構函式中刪除了 "this.name = ..."
行。
包裝例外
上述程式碼中 readUser
函式的目的是「讀取使用者資料」。過程中可能會發生不同類型的錯誤。目前我們有 SyntaxError
和 ValidationError
,但未來 readUser
函式可能會擴充,並可能產生其他類型的錯誤。
呼叫 readUser
的程式碼應該處理這些錯誤。目前它在 catch
區塊中使用多個 if
,檢查類別並處理已知錯誤,並重新擲出未知錯誤。
架構如下
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
}
}
在上述程式碼中,我們可以看到兩種錯誤類型,但可能還有更多。
如果 readUser
函式產生多種類型的錯誤,那麼我們應該問自己:我們是否真的想每次逐一檢查所有錯誤類型?
答案通常是「否」:我們希望「高出所有這些錯誤一層」。我們只想了解是否有「資料讀取錯誤」,為什麼會發生通常無關緊要(錯誤訊息會說明)。或者,更好的是,我們希望有一種方法來取得錯誤詳細資料,但只有在需要時才取得。
我們在此描述的技術稱為「包裝例外」。
- 我們將建立一個新的類別
ReadError
來表示一般的「資料讀取」錯誤。 - 函式
readUser
會捕捉其內部發生的資料讀取錯誤,例如ValidationError
和SyntaxError
,並產生一個ReadError
。 ReadError
物件會在其cause
屬性中保留對原始錯誤的參考。
然後呼叫 readUser
的程式碼只需要檢查 ReadError
,而不是各種資料讀取錯誤。如果需要錯誤的更多詳細資訊,它可以檢查其 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
的運作方式完全如說明所述,會捕捉語法和驗證錯誤,並擲出 ReadError
錯誤(未知錯誤會照常重新擲出)。
因此,外部程式碼檢查 instanceof ReadError
即可。無需列出所有可能的錯誤類型。
此方法稱為「包裝例外」,因為我們會採用「低階」例外,並將它們「包裝」成更抽象的 ReadError
。它在物件導向程式設計中廣泛使用。
摘要
- 我們可以正常繼承自
Error
和其他內建錯誤類別。我們只需要注意name
屬性,並記得呼叫super
。 - 我們可以使用
instanceof
來檢查特定錯誤。它也適用於繼承。但有時我們會收到來自第三方函式庫的錯誤物件,而且沒有簡單的方法可以取得其類別。然後,可以使用name
屬性進行此類檢查。 - 包裝例外是一種廣泛使用的技術:函式會處理低階例外,並建立高階錯誤,而不是各種低階錯誤。低階例外有時會變成該物件的屬性,例如上述範例中的
err.cause
,但這並非絕對必要。
留言
<code>
標籤,若要插入多行,請將它們包在<pre>
標籤中,若要插入超過 10 行,請使用沙盒(plnkr、jsbin、codepen…)