最近node寫的比較多,後臺應用你懂的,一個異常沒處理好,分分鐘crash給你看。在開發過程中總結了一些經驗,分享給大家
Error類
Error類是JS的原生類,在日常開發中也很常見,也很簡單,我在寫文件之前去MDN
上查了下資料:
Error類的用法很簡單,new
或者直接把Error
當成function來用都行,然後在你認為需要丟擲異常的地方throw
它。
JS中有幾種內建的Error型別,比如最常見的ReferrenceError
,都繼承自Error
,因此我們自己也可以定義自己的錯誤型別,只需要繼承Error即可,直接上MDN
的栗子:
class CustomError extends Error {
constructor(foo = 'bar', ...params) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(...params);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, CustomError);
}
// Custom debugging information
this.foo = foo;
this.date = new Date();
}
}
try {
throw new CustomError('baz', 'bazMessage');
} catch(e){
console.log(e.foo); //baz
console.log(e.message); //bazMessage
console.log(e.stack); //stacktrace
}
複製程式碼
如何捕獲異常
try...catch
就不多說了,這裡需要提一下Promise
和await
的捕獲方式
Promise
裡我們一般在最後加一個.catch
,用來處理整個Promise執行鏈路中任何可能出現的異常,比如:
Promise.resolve()
.then(() => {
console.log(a); // 這裡會出現異常
})
.then(() => {
console.log('hi'); // 這裡不會執行
})
.catch(err => {
console.log(err); // ReferenceError
});
複製程式碼
await
語法返回的也是Promise物件,不過你可以通過try...catch
語法來接住異常
async function sayHi() {
try {
let ret = await anotherPromiseFunction();
}
catch (err) {
console.log(err); // anotherPromiseFunction丟擲的異常在這裡處理
}
}
複製程式碼
如何優雅的丟擲異常
- 你需要一個
自定義錯誤類
。JS原生的錯誤型別只能定義基本的語言類異常,而我們在業務程式碼中,需要頻繁地定義、丟擲一些與業務強相關的異常,比如:
校驗驗證碼的api,驗證碼格式不對時需要丟擲一個異常,這個異常應該是跟校驗相關的,且呼叫者能清晰解讀並且能夠根據錯誤資訊做出相應處理的。
我的自定義錯誤類:
/**
* @file 錯誤型別彙總
* @author arlenyang
*/
class ApiError extends Error {
/**
* @constructor
* @param {string} code 錯誤碼
* @param {string} msg 中文描述
*/
constructor(code, msg) {
super(msg);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, CustomError);
}
this.code = code;
this.msg = msg;
}
toString() {
return `Api${this.stack}\n ${this.msg}, errCode: ${this.code}`;
}
}
// 錯誤型別
ApiError.MYSQL_QUERY_ERROR = 1;
ApiError.MYSQL_QUERY_ERROR_DESC = '查詢資料失敗';
// ......
複製程式碼
建構函式有兩個引數,code
和message
。
- code是錯誤碼,定義一個異常的簡寫,方便呼叫者判斷錯誤型別,從而處理錯誤。這在
node
裡很常見 - message是錯誤的描述,原生的Error類的建構函式本身就支援
在這個類裡,我用靜態變數的形式存放所有的錯誤碼和它的描述欄位,其實也可以放在一個單獨的存放靜態變數的檔案裡
你還可以擴充套件你的異常類做更多相關的事情,比如記錄錯誤日誌,上報或者寫入本地日誌。
另外,你還可以自定義異常的輸出,通過重寫toString
方法。還記得之前提到過的error.stack
和Error.captureStackTrace
嗎?你可以在toString
方法裡優化異常的輸出格式,加入額外的資訊,等等
- 異常不宜過度處理。如果寫每一個api或函式都去考慮所有可能丟擲異常的情形,我們應該早就累死了~ 我們需要確定哪些異常是可以拋給呼叫者處理的,這些異常通常是函式執行過程中可預見的異常(
checked exception
)。而其他異常,可能是我們的程式碼本身有bug,也可能是系統呼叫產生的error,這類異常需要呼叫者自己考慮了
舉個栗子,寫一個讀取檔案內容的api。
/**
* 讀取檔案
* @param {String} filepath 檔案路徑
* @return {Buffer} 檔案內容
*/
async function readFile(filepath) {
}
複製程式碼
根據這個api的行為可以預見幾個異常:
- 入參(filepath)為空
- 檔案路徑對應的檔案不存在
- 檔案路徑對應的檔案是否為檔案型別
至於可能出現呼叫系統讀取檔案的api出現的異常
、filepath不符合檔案路徑格式
等等的問題,都不是這個api應該考慮的範圍。實現如下:
/**
* 讀取檔案
* @param {String} filepath 檔案路徑
* @return {Buffer} 檔案內容
*/
async function readFile(filepath) {
// 檢查filepath是否為空
if (!filepath) {
// 使用自定義錯誤類
throw new ApiError(
ApiError.PARAMETER_FORMAT_ERROR,
ApiError.PARAMETER_FORMAT_ERROR_DESC,
);
}
try {
let stat = await fs.stat(filepath);
// 檢查對應的檔案是否為檔案型別
if (!stat.isFile()) {
throw new ApiError(
ApiError.FILE_FORMAT_ERROR,
ApiError.FILE_FORMAT_ERROR_DESC,
);
}
let content = await fs.readFile(filepath);
return content;
}
catch (err) {
// 檢查檔案是否存在
if (err.code === 'ENOENT') {
throw new ApiError(
ApiError.FILE_NOT_FOUND_ERROR,
ApiError.FILE_NOT_FOUND_ERROR_DESC,
);
}
throw err;
}
}
複製程式碼
-
對於promise的異常處理,千萬不要為了'安全起見'把所有函式都
.catch
,這可能會導致exception被吞掉,查錯時找不到異常資訊-
對於需要catch的promise,儘量先處理異常,處理不了的,再向後拋
-
用
Promise.reject(error)
代替throw error
,更優雅 -
promise的
then(resolve, reject)
和then(resolve, null).catch()
的區別 -
promise.catch
裡如果沒有再reject
或throw
,之後邏輯會走到resolve
裡而非reject
Promise.resolve() .then(() => { console.log(a); // 這一行報錯,會被catch接住 }) .catch(err => { console.log(err); return 1; }) .then(ret => { console.log(ret); // 會執行,且列印 1 }); 複製程式碼
-