浏览器原生能力系列 - 错误和异常

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);
}

可以看到正确的输出和错误的输出泾渭分明:
1506302611_100_w491_h104.png
我一直觉得这样的写法有几个好处:

  1. 可以将异常逻辑和主流程逻辑分开,从而使可读性更加清晰。
  2. Erlang 中有一句话叫做:“Let it crash”。 - 这不是说让程序真的崩溃了,而是提醒开发者小心处理每一个错误,有的时候崩溃了会更加容易发现问题所在。
  3. 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