最近node写的比较多,后台应用你懂的,一个异常没处理好,分分钟crash给你看。在开发过程中总结了一些经验,分享给大家
Error类
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 });
-