JavaScript 中的错误处理机制

橘子小睿

错误处理在开发和调试过程中都显得尤为重要。有些没有进行错误处理的应用,直接就将浏览器的错误展示给了用户,极大的降低了用户体验。比如有些很 low 的网站,打开某些页面就直接弹出 "object" 这样的错误,用户看到之后一脸懵逼,心想这是什么鬼?让人感觉极其的不专业。可见错误处理对一个应用来说是多么的重要。

这篇文章主要是给大家科普一些关于错误处理的知识,让大家在脑海中有一个概览。下一篇文章中我会结合具体的项目以及当前主流的一些框架,比如react, redux,来更深入的介绍如何运用这些框架去封装一整套错误处理的解决方案。

error 对象

Error 构造对象可以实例化一个 error 对象 (也就是Error 实例),而 error 对象就是一个包含了错误信息的对象。当代码解析或者运行时发生错误,javascript 引擎就会自动产生并抛出一个 error 对象, 然后程序就中断在发生错误的地方。

示例:

const error = new Error('Whoop!');
error.message; // Whoop!
error.name; // Error
error.stack; // "Error: Whoops! at <anonymous>:1:13"

我们常用的 messagename 都是 error 的标准属性,由于各个浏览器厂商对 error 进行了不同的扩展,所以在不同的浏览器中,error 也有不同的属性和方法, 非标准属性中我们常用的是 stack 属性(很多浏览器都扩展了这一属性), 它用来表示栈跟踪信息。

属性 含义
message 错误信息
name 错误类型
constructor 指定一个函数用来创建实例的原型,也就是指定构造器(创建自定义 Error 会用到)
stack (非标准) 栈跟踪信息

error 类型

除了普通的 Error 构造对象以外, javascript 还实现了其他几种主要的 error 构造对象1.

类型 解析 实例
EvalError eval错误。跟全局函数 eval() 有关的错误,在 ES5 之后已经不再出现了
InternalError 内部错误。由 JavaScript 引擎抛出的错误
RangeError 范围错误。发生在一个数值或参数超出其合法范围,主要包括超出数组范围或者超出数字取值范围 new Array(-1); (1234).toExponential(21);
ReferenceError 引用错误。通常是由于引用了一个不存在的值。 a.toString();
SyntaxError 语法错误。 a ? a+1;
TypeError 类型错误。通常是因为在执行特定的类型操作时,变量的类型不符合要求。例如变量中保存着意外类型,访问不存在的方法等。 var a = new {}; var a = {a:1}; a.reverse(); // 对象并没有 reverser 方法
URIError decodeURI() 或者 encodeURI() 传入非法参数时,也包括 encodeURIComponent() 和 decodeURIComponent() decodeURI('http://www.test.com&%'); encodeURIComponent('uD800');

encodeURI 和 encodeURIComponent 的区别

这里顺便说一下 encodeURIencodeURIComponent 的区别。已经了解的同学可以忽略这一小部分,继续往前面看。

encodeURI 是对统一资源标识符 (URI) 全部编码,而 encodeURIComponent 对统一资源标识符 (URI) 部分编码

假设一个 URI 是一个完整的 URI, 那么我们不必对那些在 URI 中保留的并且带有特殊含义的字符进行编码。由于 encodeURI 会替换掉所有字符,但是却不包含一些保留字符,如 "&", "+", "=" 等(这些字符在 GET 和 POST 请求中是特殊字符,需要被编码),所以 encodeURI 本身无法产生能使用与 HTTP GET 或者 POST 请求的 URI。但是我们可以使用 encodeURIComponent 来对这些字符进行编码。

encodeURIComponent 转义除了字母、数字、(、)、.、!、~、*、'、-和_之外的所有字符。
为了避免服务器收到不可预知的请求,对任何用户输入的作为 URI 部分的内容都需要用 encodeURIComponent 进行转义。

抛出和捕获错误 throw and try...catch

通常使用 throw 语句抛出错误,并用 try...catch 进行捕获。通常会把所有可能会抛出错误的代码都放在 try 语句块中,而把那些用于错误处理的代码放在 catch 块中。

throw 语句

throw 过程是阻塞的,程序会中断在第一个抛出错误的地方,所以后面的代码不会执行。

throw new SyntaxError('this is syntax error'); 
throw 123; // 不执行
throw 'hi there'; // 不执行
throw true;  // 不执行

catch 语句

catch 代码块捕获错误之后,程序不会中断,会按照正常流程继续执行下去。

try {
  throw new Error('Whoops!');
} catch (e) {
  console.log(e.name + ':' + e.message);
}
console.log('hello!');

// Error:Whoops!
// hello!

finally 语句

finallytry...catch 中是可选的,但是一旦使用了它,它里面的代码就一定会被执行,也就是说不管 try 语句块中的代码是否正常执行,finnaly 都会被执行。正如下面的代码, 即使在 try 中资源被阻塞,由于我们在 finnaly 中执行了关闭操作,文件最后还是会被关闭。

openMyFile()
try {
   // 阻塞资源
   writeMyFile(theData);
} finally {
   closeMyFile(); // 始终会关闭资源
}

处理一个特定的错误。

    try {
      foo.bar();
    } catch (e) {
      switch (e.name) {
        case 'RangeError':
          //do something
          console.log('RangeError: ' + e.message);
          break;
        case 'ReferenceError':
          //do something
          console.log('ReferenceError: ' + e.message);
          break;
        default:
          console.log(e.name + ':' + e.message);
      }
    }

error 事件

任何没有 catch 的错误都会触发 window 对象的 error 事件。

error 事件可以接收三个参数:错误消息、错误所在的 URL 和行号。你可以通过以下两种方式给 window 绑上 error 事件2

  // message: 错误消息, source: 发生错误文件的 URL, lineno: 错误行号

  // 方法一
  window.onerror = function(messageOrEvent, source, lineno, colno, error) {
    alert(messageOrEvent, source, lineno, colno, error);
  }
  
  or 
  
  window.onerror = console.log;
  throw new Error('whoops!');
  
  // 方法二 
  window.addEventListener('error', function(errorEvent){
      alert(errorEvent.error);
  });

在实际情况中,error 事件并不常用(但是在某些情况下,如微信端的调试,error 事件还是挺有用的),因为我们还是希望所有的异常都能得到很好的处理,而不是把错误交给浏览器。但有的时候并不是所有的错误都能够被扑获,并且某些业务场景会使用到追踪浏览器报错的工具,这时候可能就需要将浏览器的错误抛出去,所以在这种情况下也需要去全局监听 error 事件。

自定义错误类型 Custom Error Types

创建一个自定义类 CustomError, 以方便去扩展更多的自定义 Error

CustomError.js

class CustomError extends Error {
  constructor(message) {
    super(message);
    this.name = this.constructor.name;
    if (typeof Error.captureStackTrace === 'function') {
      // 在浏览器领域,除了使用V8引擎的 Chrome,
      // 其它浏览器中不存在 Error.captureStackTrace()这一接口,
      // 所以在这里做一个条件判断。
      Error.captureStackTrace(this, this.constructor); // 返回调用堆栈信息, 用于在 error 对象上添加合理的 stack 属性。
    } else {
      this.stack = new Error(message).stack;
    }
  }
}

Error.captureStackTrace

Error.captureStackTrace 是用来在 targetObject 中添加一个 .stack 属性。对该属性进行访问时,将以字符串的形式返回 Error.captureStackTrace() 语句被调用时的代码位置信息(即:调用栈历史)。

Error.captureStackTrace(targetObject[, constructorOpt])

除了 targetObject, captureStackTrace 还接受一个类型为 function 的可选参数 constructorOpt,当传递该参数时,调用栈中所有 constructorOpt 函数之上的信息(包括 constructorOpt 函数自身),都会在访问 targetObject.stack 时被忽略。当需要对终端用户隐藏内部的技术细节时, constructorOpt 参数会很有用。

扩展自定义 Error 类型

通过基类 CustomError,我们可以创建出更多的自定义 Error, 比如下面的 HttpRequestErrorLoginExpiredError

HttpRequestError.js

class HttpRequestError extends CustomError {
  constructor(message, requestId, code, httpStatusCode) {
    const defaultAPIErrorMessage = () => {
      // 检查是否有网
      return window.navigator.onLine ? 'Something wrong' : 'No connection';
    };

    message = message || defaultAPIErrorMessage();
    super(message);

    this.requestId = requestId;
    this.code = code;
    this.httpStatusCode = httpStatusCode;
  }
}

throw new HttpRequestError(null, 'requestId', 'code', 'httpStatusCode');

LoginExpiredError.js

class LoginExpiredError extends CustomError {
  constructor(message) {
    message = message || 'Your session has expired!';

    super(message);
  }
}

throw new LoginExpiredError();

当我们创建了各种自定义 Error 之后,我们可以在不同的场景去使用它们了,比如在 http 请求失败的时候抛出 HttpRequestError,并弹出对话框提示用户。在用户登录过期之后,抛出 LoginExpiredError,弹出对话框,并自动 logout 等等。通过抛出不同的 Error 类型,才能让我们进行不同的扑获处理。

  const deffer = Q.deffer();

  if (expired) {
    deffer.reject(new LoginExpiredError()); // 通过 promise 抛出异常
  }

  if (error instanceof LoginExpiredError) { // 扑获异常
    logout();
  }

结尾

关于 Error 的介绍就先讲到这里。


  1. Error in MDN
  2. window.onerror
阅读 6.4k

橘子小睿的前端杂记
专注前端开发

前端开发者

657 声望
73 粉丝
0 条评论

前端开发者

657 声望
73 粉丝
文章目录
宣传栏