當我們在開發某些東西時,經常會需要我們自己的 error 類來反映在我們的任務中可能出錯的特定任務。對于網絡操作中的 error,我們需要 HttpError
,對于數據庫操作中的 error,我們需要 DbError
,對于搜索操作中的 error,我們需要 NotFoundError
,等等。
我們自定義的 error 應該支持基本的 error 的屬性,例如 message
,name
,並且最好還有 stack
。但是它們也可能會有其他屬于它們自己的屬性,例如,HttpError
對象可能會有壹個 statusCode
屬性,屬性值可能爲 404
、403
或 500
等。
JavaScript 允許將 throw
與任何參數壹起使用,所以從技術上講,我們自定義的 error 不需要從 Error
中繼承。但是,如果我們繼承,那麽就可以使用 obj instanceof Error
來識別 error 對象。因此,最好繼承它。
隨著開發的應用程序的增長,我們自己的 error 自然會形成形成壹個層次結構(hierarchy)。例如,HttpTimeoutError
可能繼承自 HttpError
,等等。
例如,讓我們考慮壹個函數 readUser(json)
,該函數應該讀取帶有用戶數據的 JSON。
這裏是壹個可用的 json
的例子:
let json = `{ "name": "John", "age": 30 }`;
在函數內部,我們將使用 JSON.parse
。如果它接收到格式不正確的 json
,就會抛出 SyntaxError
。但是,即使 json
在語法上是正確的,也不意味著該數據是有效的用戶數據,對吧?因爲它可能丟失了某些必要的數據。例如,對用戶來說,必不可少的是 name
和 age
屬性。
我們的函數 readUser(json)
不僅會讀取 JSON,還會檢查(“驗證”)數據。如果沒有所必須的字段,或者(字段的)格式錯誤,那麽就會出現壹個 error。並且這些並不是 SyntaxError
,因爲這些數據在語法上是正確的,這些是另壹種錯誤。我們稱之爲 ValidationError
,並爲之創建壹個類。這種類型的錯誤也應該包含有關違規字段的信息。
我們的 ValidationError
類應該繼承自 Error
類。
Error
類是內建的,但我們可以通過下面這段近似代碼理解我們要擴展的內容:
// JavaScript 自身定義的內建的 Error 類的“僞代碼” class Error { constructor(message) { this.message = message; this.name = "Error"; // (不同的內建 error 類有不同的名字) this.stack = <call stack>; // 非標准的,但大多數環境都支持它 } }
現在讓我們從其中繼承 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); // 壹個嵌套調用的列表,每個調用都有對應的行號 }
請注意:在 (1)
行中我們調用了父類的 constructor。JavaScript 要求我們在子類的 constructor 中調用 super
,所以這是必須的。父類的 constructor 設置了 message
屬性。
父類的 constructor 還將 name
屬性的值設置爲了 "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
又處理來自 JSON.parse
的內建 SyntaxError
。
請看壹下我們是如何使用 instanceof
來檢查 (*)
行中的特定錯誤類型的。
我們也可以看看 err.name
,像這樣:
// ... // instead of (err instanceof SyntaxError) } else if (err.name == "SyntaxError") { // (*) // ...
使用 instanceof
的版本要好得多,因爲將來我們會對 ValidationError
進行擴展,創建它的子類型,例如 PropertyRequiredError
。而 instanceof
檢查對于新的繼承類也適用。所以這是面向未來的做法。
還有壹點很重要,在 catch
遇到了未知的錯誤,它會在 (**)
行將該錯誤再次抛出。catch
塊只知道如何處理 validation 錯誤和語法錯誤,而其他錯誤(由代碼中的拼寫錯誤或其他未知原因導致的)應該被扔出(fall through)。
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; } } // 用法 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,將其再次抛出 } }
這個新的類 PropertyRequiredError
使用起來很簡單:我們只需要傳遞屬性名:new PropertyRequiredError(property)
。人類可讀的 message
是由 constructor 生成的。
請注意,在 PropertyRequiredError
constructor 中的 this.name
是通過手動重新賦值的。這可能會變得有些乏味 —— 在每個自定義 error 類中都要進行 this.name = <class name>
賦值操作。我們可以通過創建自己的“基礎錯誤(basic error)”類來避免這種情況,該類進行了 this.name = this.constructor.name
賦值。然後讓所有我們自定義的 error 都從這個“基礎錯誤”類進行繼承。
讓我們稱之爲 MyError
。
這是帶有 MyError
以及其他自定義的 error 類的代碼,已進行簡化:
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 是對的 alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
現在自定義的 error 短了很多,特別是 ValidationError
,因爲我們擺脫了 constructor 中的 "this.name = ..."
這壹行。
在上面代碼中的函數 readUser
的目的就是“讀取用戶數據”。在這個過程中可能會出現不同類型的 error。目前我們有了 SyntaxError
和 ValidationError
,但是將來,函數 readUser
可能會不斷壯大,並可能會産生其他類型的 error。
調用 readUser
的代碼應該處理這些 error。現在它在 catch
塊中使用了多個 if
語句來檢查 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
函數會産生多種 error,那麽我們應該問問自己:我們是否真的想每次都壹壹檢查所有的 error 類型?
通常答案是 “No”:我們希望能夠“比它高壹個級別”。我們只想知道這裏是否是“數據讀取異常” —— 爲什麽發生了這樣的 error 通常是無關緊要的(error 信息描述了它)。或者,如果我們有壹種方式能夠獲取 error 的詳細信息那就更好了,但前提是我們需要。
我們所描述的這項技術被稱爲“包裝異常”。
我們將創建壹個新的類 ReadError
來表示壹般的“數據讀取” error。
函數readUser
將捕獲內部發生的數據讀取 error,例如 ValidationError
和 SyntaxError
,並生成壹個 ReadError
來進行替代。
對象 ReadError
會把對原始 error 的引用保存在其 cause
屬性中。
之後,調用 readUser
的代碼只需要檢查 ReadError
,而不必檢查每種數據讀取 error。並且,如果需要更多 error 細節,那麽可以檢查 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
正如所描述的那樣正常工作 —— 捕獲語法和驗證(validation)錯誤,並抛出 ReadError
(對于未知錯誤將照常再次抛出)。
所以外部代碼檢查 instanceof ReadError
,並且它的確是。不必列出所有可能的 error 類型。
這種方法被稱爲“包裝異常(wrapping exceptions)”,因爲我們將“低級別”的異常“包裝”到了更抽象的 ReadError
中。它被廣泛應用于面向對象的編程中。
我們可以正常地從 Error
和其他內建的 error 類中進行繼承。我們只需要注意 name
屬性以及不要忘了調用 super
。
我們可以使用 instanceof
來檢查特定的 error。但有時我們有來自第三方庫的 error 對象,並且在這兒沒有簡單的方法來獲取它的類。那麽可以將 name
屬性用于這壹類的檢查。
包裝異常是壹項廣泛應用的技術:用于處理低級別異常並創建高級別 error 而不是各種低級別 error 的函數。在上面的示例中,低級別異常有時會成爲該對象的屬性,例如 err.cause
,但這不是嚴格要求的。
重要程度: 5
創建壹個繼承自內建類 SyntaxError
的類 FormatError
。
它應該支持 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(因爲它繼承自 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