浏览器原生能力系列 - 错误和异常
Error 对象在 JS 中貌似是一个长期被忽略的对象,很多人宁愿用别的方法来描述错误,例如一个特别类型的返回值,或者通过返回码,但其实这个对象从 ES1 里引入开始就带来了无限的可能性。
笔者开发代码的时候,一直偏好将函数的正常输出和异常分开,类似这样:
function mustBeEqual(a, b) {
if (a !== b) {
throw new Error(`${a} is not equal with ${b}`);
}
return true;
}
try {
const equal = mustBeEqual(1, 1);
console.log('1 is equal with 1');
const equal2 = mustBeEqual(1, 2);
console.log('1 is equal with 2');
} catch (err) {
console.error(err.message);
console.error(err.stack);
}
可以看到正确的输出和错误的输出泾渭分明:
我一直觉得这样的写法有几个好处:
- 可以将异常逻辑和主流程逻辑分开,从而使可读性更加清晰。
- Erlang 中有一句话叫做:“Let it crash”。 - 这不是说让程序真的崩溃了,而是提醒开发者小心处理每一个错误,有的时候崩溃了会更加容易发现问题所在。
- Error 对象的一些属性,例如 stack 对于发现问题所在位置其实非常有帮助,它对于还原问题帮助非常大。
Error 对象的具体参数请参考一下 MDN,就此不再多述: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error
我只想说 Error 的主要用法。
继承出业务错误类型。
在项目开发中,会碰到各种各样的网络、数据库、外部 RPC 调用,各种问题出现之后难以以一种统一的方案去解决。
此时其实可以通过继承几个业务错误,把底层错误转换为自己项目中所使用的,二次抛出后进行处理。
例如下面代码,摘取自 http://git.code.oa.com/weplus/serverplus/blob/master/src/errors.ts 这里是 Typescript 的语法。
/**
* Base Error
*/
class BaseError extends Error {
public status: number;
public code?: number;
}
/**
* Request not passed validation will trigger
*/
class BadRequestError extends BaseError {
public status = 400;
}
/**
* The user was not login
*/
class UnauthorizedError extends BaseError {
public status = 401;
}
export {
BadRequestError,
UnauthorizedError,
}
这样做有几个好处:
上层可以对输出进行统一处理
底层输出只有两种:正常返回和异常,所有的异常都是一个错误的对象,这样就可以简化处理逻辑,对正常输出走业务逻辑,而错误会全部进入 catch 段进行异常处理。
在上面的例子中,HTTP 的状态码就是依靠错误的 status 属性进行确定,当某个业务流程需要返回一个错误时,直接 throw 即可。
摘自 http://git.code.oa.com/weplus/serverplus/blob/master/src/base-model-controller.ts
/**
* Get single record method
*/
public async get(req, res) {
let authenticated;
try {
authenticated = await this.apiAuthenticate(req);
} catch (err) {
throw err;
}
if (typeof authenticated === 'boolean' && !authenticated) {
throw new this.errors.ForbiddenError('Permission denied', {
requestId: req.headers['X-Request-Id'],
});
}
const id = req.params.id;
const result = await this.__get(req, id, req.query, req.params);
return result;
}
上面的路由层接收到一个错误,而不是一个正常的返回值时,就会将它作为错误进行输出。
上层也可以通过继承链,找到具体错误的具体类型和原因。
通过 instanceof 去找错误,效率比通过字符串高出数倍不止,可以将程序内的错误,和给用户的提示分开,可以根据不同的错误类型,进行不同的处理。
const err = new TypeError('Something went wrong');
err instanceof TypeError
// true
err instanceof Error
// true
err instanceof RangeError
// false
err.message
// "Something went wrong"
因为异常本身是可被触发的,它可以干更多事情,例如将错误自己上报给错误监测系统。
在 http://git.code.oa.com/weplus/serverplus/blob/master/src/errors.ts 还可以看到,在基类错误的构建函数中,还有一段 Raven.captureException()
的上报方法,这样我们在这里利用了错误的构建时,将自身连带附加信息一起上报给了 Sentry(Sentry 是什么情参考《Web 前后端错误上报及问题跟踪》)。
/**
* Base Error
*/
class BaseError extends Error {
public status: number;
public code?: number;
constructor (message, context: any = {}) {
super(message);
Object.setPrototypeOf(this, BaseError.prototype);
if (!config.sentry) {
return this;
}
const extra = {
status: this.status,
code: this.code,
...context,
};
Raven.captureException(this, {
extra,
});
}
}
这样一来,当服务器发生异常时候,不用改动主逻辑代码,自动实现了错误上报,这样可以作为一个单独的错误上报层在项目中使用,而且它在上报的时候,还能带上上下文,这样对于错误还原帮助巨大。
还是上面那个例子,这里在非法请求时在返回 Permission Denied 的同时,把前端请求过来的 requestId 一起附带上报,前端整合 Sentry 后可以做全链路的错误还原。
if (typeof authenticated === 'boolean' && !authenticated) {
throw new this.errors.ForbiddenError('Permission denied', {
requestId: req.headers['X-Request-Id'],
});
}
面向错误进行应用开发
这种开发模式可以根据场景使用,需要将之前的思维进行小幅度调整,之前 res.code === 0 && doSomethingRight() || doSomethingWrong()
的方式得用 try catch 重新进行梳理,但这样做可以让主线流程代码变得很干净,减少大量的 if else。
面向错误进行开发,需要控制好 try catch 的颗粒度,理论上都是越细越好的,如果一个大的 try 都裹在一起,任何一处发生问题后都会走入 catch 环节这会加大判断错误问题发生位置的难度,尤其是在某些未对底层错误进行二次捕获抛出的架构中会更加严重。
过去和未来
在早期的浏览器引擎中, try catch 方式是比较低效无法被优化的,不过现在新版的 V8 引擎 TurboFan 已经对 try catch 进行了大幅度调整,之前无法被优化的代码也可以以最优方式运行,而服务器端截止本文完稿,搭载 TurboFan 的 Node 8 已经进入 Stable,预期 10 月份进入 LTS,这会让 try catch 的使用更加放心。
面向错误进行开发这种开发模式其实在 Java、Python 或其它语言中已经非常普遍,但在 Javascript 领域目前感觉比较好的是 NodeJS 上的 ORM 库 Sequelize,它里面对错误都进行了良好封装。
我希望未来这样比较小,但是有用的开发模式能更加普及。
版权所有丨转载请注明出处:https://kxq.io/archives/liu-lan-qi-yuan-sheng-neng-li-xi-lie--cuo-wu-he-yi-chang