JS異常處理

YDSS發表於2018-05-08

最近node寫的比較多,後臺應用你懂的,一個異常沒處理好,分分鐘crash給你看。在開發過程中總結了一些經驗,分享給大家

Error類

Error類是JS的原生類,在日常開發中也很常見,也很簡單,我在寫文件之前去MDN上查了下資料:

JS異常處理

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就不多說了,這裡需要提一下Promiseawait的捕獲方式

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丟擲的異常在這裡處理
	}
}
複製程式碼

如何優雅的丟擲異常

  1. 你需要一個自定義錯誤類。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 = '查詢資料失敗';

// ......

複製程式碼

建構函式有兩個引數,codemessage

  • code是錯誤碼,定義一個異常的簡寫,方便呼叫者判斷錯誤型別,從而處理錯誤。這在node裡很常見
  • message是錯誤的描述,原生的Error類的建構函式本身就支援

在這個類裡,我用靜態變數的形式存放所有的錯誤碼和它的描述欄位,其實也可以放在一個單獨的存放靜態變數的檔案裡

你還可以擴充套件你的異常類做更多相關的事情,比如記錄錯誤日誌,上報或者寫入本地日誌。

另外,你還可以自定義異常的輸出,通過重寫toString方法。還記得之前提到過的error.stackError.captureStackTrace嗎?你可以在toString方法裡優化異常的輸出格式,加入額外的資訊,等等

  1. 異常不宜過度處理。如果寫每一個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;
	}
}
複製程式碼
  1. 對於promise的異常處理,千萬不要為了'安全起見'把所有函式都.catch,這可能會導致exception被吞掉,查錯時找不到異常資訊

    1. 對於需要catch的promise,儘量先處理異常,處理不了的,再向後拋

    2. Promise.reject(error)代替throw error,更優雅

    3. promise的then(resolve, reject)then(resolve, null).catch()的區別

    4. promise.catch裡如果沒有再rejectthrow,之後邏輯會走到resolve裡而非reject

      Promise.resolve()
      	.then(() => {
      		console.log(a); // 這一行報錯,會被catch接住
      	})
      	.catch(err => {
      		console.log(err);
      		return 1;
      	})
      	.then(ret => {
      		console.log(ret); // 會執行,且列印 1
      	});
      複製程式碼

REFERENCE

  1. Java 異常進階
  2. 處理JavaScript異常的正確姿勢
  3. Error