浅析错误处理

184 阅读2分钟

已知错误和未知错误

错误可以分为已知错误和未知错误两种,已知错误即看代码可以分析出的错误,比如传参的类型不对,这类错误一般直接处理就可以了,比如增加类型校验。而需要进行错误处理的是未知错误,毕竟没人可以保证自己的代码不会出错。就算有,那调别人写的包总有可能出错吧。
对于未知错误,我们需要减少错误的发生,还需要建立错误处理机制,以处理发生的错误。

如何减少错误的发生

  1. 使用静态代码分析器,比如使用TypeScript(可以避免数据类型错误)。
  2. 只用严格相等和严格不相等,除了判断一个变量是否不为null且不为undefined时(毕竟jQuery源码是这样写的),可以用以下写法:
if (abc != null) {
  ...
}
  1. 在if、for或while等流控制语句中使用非布尔值,总之,就是尽可能不用隐式转换。
  2. 在拼接URL时对参数部分使用encodeURIComponent()
function addQueryStringArg(url:string, name:string, value:string):string {
  const suffix = url.includes('?')?'&':'?';
  const encodeName = encodeURIComponent(name);
  const encodeValue = encodeURIComponent(value);
  return `${url}${suffix}${encodeName}=${encodeValue}`;
}

建立错误处理机制

在Vue源码中有大量错误处理的代码,我们往往可以根据这些报错清晰地知道错在哪里,并很快修复。所以比起浏览器抛出的意味不明的错误,显然还是自己抛出一个清晰的错误更为直观,所以,在 前端建立一个错误处理机制十分有必要。为了建立这个机制,我们需要做到三点:

  1. 捕获错误
  2. 提供简洁易懂的提示
  3. 记录错误,减少错误的发生
// vue源码
// src/platforms/web/entry-runtime-with-compiler.js 36:58
if (template) {
  if (typeof template === 'string') {
    if (template.charAt(0) === '#') {
      template = idToTemplate(template)
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && !template) {
        warn(
          `Template element not found or is empty: ${options.template}`,
          this
        )
      }
    }
  } else if (template.nodeType) {
    template = template.innerHTML
  } else {
    if (process.env.NODE_ENV !== 'production') {
      warn('invalid template option:' + template, this)
    }
    return this
  }
} else if (el) {
  template = getOuterHTML(el)
}

捕获错误

一般我们使用try/catch语句捕获错误,以下有几点需要注意:

  1. try/catch语句中可选的finally子句始终运行,就算你在try块或者catch块中写了return,只要代码中包含了finally子句,try块或catch块中的return语句就会被忽略。
  2. finally子句和catch块中只有一个是必需的。
  3. 在try/catch语句的catch块中,可以使用instanceof操作符确定错误的类型。

ECMA-262定义了以下8种错误类型:

序号类型备注
1Error基类,其他错误类型继承它,一般是开发者抛出的自定义错误
2InternalError内部错误
3EvalError基本上,只要不把eval()当成函数调用就会报告该错误
4RangeError会在数值越界时抛出
5ReferenceError会在找不到对象时抛出
6SyntaxError语法错误
7TypeError类型错误
8URIError只会在使用encodeURI()或decodeURI()但传入了格式错误的URI时发生

而很明显try/catch语句只适合捕获局部的错误,而如果要监听全局的错误,显然还是使用window的error事件。任何没有被try/catch语句处理的错误都会在window上触发error事件。(该事件是浏览器早期支持的事件,为保持向后兼容,很多浏览器保持了其格式不变。)

注意这里只能捕获同步错误,promise的异步错误需要用catch()方法处理,除非这个promise前加了await。

window.onerror = (message/* 错误消息 */, url/*发生错误的url*/, line/* 行号 */) => {
  // 为保证跨浏览器兼容,最好只依赖message属性。
  console.log(message);
  return false; // 可以返回false来阻止浏览器默认报告错误的行为
};

提供简洁易懂的提示

为了保证提供简洁易懂的提示,我们可以通过throw抛出自定义异常,throw操作符必须有一个值,但值的类型不限。

throw new Error('出错了!');

通过集成Error也可以创建自定义的错误类型。创建自定义错误类型时,需要提供name属性和message属性,比如:

class CustomError extends Error {
  constructor (message) {
    super(message);
    this.name = 'CustomError';
    this.message = message;
  }
}

throw new CustomError('My message');

每种错误类型的构造函数都只接收一个参数,就是错误消息。集成Error的自定义错误类型会被浏览器当成其他内置错误类型。自定义错误类型有助于在捕获错误时更准确地区分错误。

记录错误,减少错误的发生

Web应用程序开发中的一个常见做法是建立中心化的错误日志存储和跟踪系统,为了记录所有页面的报错,上边的代码可以改写成:

function logError(sev, msg) {
  let img = new Image();
  let encodeSev = encodeURIComponent(sev);
  let encodeMsg = encodeURIComponent(msg);
  img.src = `log.php?sev=${encodeSev}&msg=${encodeMsg}`;
}

window.onerror = (message/* 错误消息 */, url/*发生错误的url*/, line/* 行号 */) => {
  // 为保证跨浏览器兼容,最好只依赖message属性。
  logError(url, message);
  return false; // 可以返回false来阻止浏览器默认报告错误的行为
};

通过记录报错信息,我们可以知道报了什么错,其影响范围有多大,从而清楚需要除了哪些错误。

(附录)调试技术

方法备注
console.log()记录常规消息
console.­info()记录消息性内容
console.error()记录错误消息
console.warn()记录警告消息
debugger用于打断点
一般在修复bug时都会结合debugger和console.log()来排查问题。
小技巧:检查一个元素,或者点击开发者工具中Elements中的一个元素之后,即可在控制台中使用$0引用该节点的JavaScript实例。