深入理解 JavaScript 错误处理机制

栏目: jQuery · 发布时间: 5年前

内容简介:作者包龙星(企业代号名),目前负责贝壳找房河图项目的前端研发工作。

深入理解 JavaScript 错误处理机制

作者包龙星(企业代号名),目前负责贝壳找房河图项目的前端研发工作。

1 错误分类

javascript错误,可分为编译时错误,运行时错误,资源加载错误。本文着重讨论一下 运行时错误 资源加载错误

1.1 js运行时错误

javascript提供了一种捕获运行时错误的捕获机制。如果代码能够捕获潜在的错误,并能适当处理,就能确保代码不会在运行时产生意想不到的错误,给用户造成困扰,这也意味着代码的质量是非常高的。

1.1.1 Error实例对象

javaScript解析或运行时,一旦发生错误,引擎就会抛出一个错误对象。JavaScript原生提供Error构造函数,所有抛出的错误都是这个构造函数的实例。

Error实例对象的三个属性:

  • message 错误提示信息

  • name 错误名称

  • stack 错误的堆栈

例如下面的代码,打印错误实例对象,可以得到 message name stack 信息:

1var err = new Error('出错了');
2console.dir(err)

深入理解 JavaScript 错误处理机制

控制台输出

上面的例子中, err 是一个对象( object )类型, 拥有 message、stack 两个属性,还有一个原型链上的属性 name ,来自于构造函数 Error 的原型。

1.1.2 6种错误类型

以下6种错误类型都是Error对象的派生对象。在javascript中, 数组array、函数function都是特殊的对象:

1)SyntaxError 语法错误    
SyntaxError是代码解析时发生的语法错误。例如,写了一个错误的语法 var a =

1function fn() {
2    var a = 
3}
4// Uncaught SyntaxError: Unexpected token }
5fn() 

2)TypeError 类型错误  
TypeError是变量或者参数不是预期类型时发生的错误。例如在number类型上调用array的方法。

1var n = 1234
2// Uncaught TypeError: a.concat is not a function
3a.concat(9) 

3)RangeError 范围错误  
RangeError是一个值超过有效范围发生的错误。例如设置数组的长度为一个负值。

1// 数组长度不得为负数
2new Array(-1)
3// Uncaught RangeError: Invalid array length

4)ReferenceError 引用错误  
ReferenceError是引用一个不存在的变量时发生的错误。

1// Uncaught ReferenceError: mmm is not defined
2console.log(mmm)

5)EvalError eval错误
eval函数没有被正确执行时,会抛出EvalError错误。该错误类型已经不再使用了,只是为了保证与以前代码兼容,才继续保留。

1// Uncaught TypeError: eval is not a constructor
2new eval()
3// 不会报错
4eval = () => {}

6)URIError URL错误  
URIError指调 decodeURI encodeURI decodeURIComponent encodeURIComponent escape unescape 时发生的错误。

1// URIError: URI malformed
2    at decodeURIComponent 
3decode
4decodeURIComponent('%')

1.2 资源加载错误

当以下标签(不包括 <link> ),加载资源出错时,会发生资源加载错误。

1<img>, <input type="image">, <object>, <script>, <style> , <audio>, <video>
2

资源加载错误可以用onerror事件监听。

1<img onerror="handleError">

资源加载错误不会冒泡,只能在事件流捕获阶段获取错误。

1# 第三个参数默认为false, 设为true, 表示在事件流捕获阶段捕获
2window.addEventListener('error', handleError, true)

当加载跨域资源时,不会报错,需要在元素上添加 crossorigin,同时服务器需要在response header中,设置Access-Control-Allow-Origin为*或者允许的域名。

1<script src="xxx" crossorigin></script>

2 错误捕获

参考阿里开源框架jstracker源码

 1// 阿里 jstracker 核心源码
 2// 捕获资源加载错误
 3window.addEventListener('error', handleError, true)
 4
 5/**
 6* 捕获js运行时错误
 7* 函数参数:  
 8* message: 错误信息(字符串)
 9* source: 发生错误当脚本URL
10* lineno: 发生错误当行号
11* colno: 发生错误当列号
12* error: Error对象
13**/
14window.onerror = function(message, source, lineno, colno, error) { ... }
15
16// 捕获vue中的错误, 重写console.error
17console.error = () => {}

上面的代码, 不是很严谨, 如果用户在代码中也写了window.onerror, 会被覆盖, 导致错误没有正常上报。

3 throw

MDN关于throw的定义

throw语句用来抛出一个用户自定义的异常。当前函数的执行将被停止(throw之后的语句将不会执行),并且控制将被传递到调用堆栈中的第一个catch块。如果调用者函数中没有catch块,程序将会终止。

MDN上关于throw的定义,翻译得不够准确,对于“程序将会终止”,我有不同的看法,下面请听我的分析。  
"throw 之后的语句将不会执行。",这句话比较容易理解,例如:

1console.log(1)
2throw 1234
3// 下面这行代码不会执行
4console.log(2)

"如果调用者函数中没有catch块,程序将会终止",这句话是有问题的。下面用代码来推翻这个结论:

 1<button id="btn-1">打印1</button> 
 2<button id="btn-2">打印2</button>
 3<script>
 4  function log(n) {
 5    console.log(n)
 6  }
 7
 8  document.getElementById('btn-1').onclick = function() {
 9    log(1)
10  }
11
12  // 每1s打印一次
13  setInterval(() => {
14    log('setInterval依然在执行')
15  }, 1000)
16
17  throw new Error('手动抛出异常')
18
19  // 这段代码不会执行
20  document.getElementById('btn-2').onclick = function() {
21    log(2)
22  }
23</script>

运行上面的代码,控制台首先会抛出错误,然后每秒打印"setInterval依然在执行"

深入理解 JavaScript 错误处理机制

点击btn-1,打印1;点击but-2,无反应。

这就说明:

throw 之后,程序没有停止运行 。

结论:throw之后的语句不会执行,并且控制将被传递到调用堆栈中的第一个catch块。如果调用者函数中没有catch块,程序也不会停止,throw之前的语句依旧在执行。

4 try...catch...finally

try/catch的作用是将可能引发错误的代码放在try块中,在catch中捕获错误,对错误进行处理,选择是否往下执行。

4.1 try 代码块中的错误,会被catch捕获,如果没有手动抛出错误,不会被window捕获

1try {
2  throw new Error('出错了!');
3} catch (e) {
4  console.dir(e);
5  throw e
6}

深入理解 JavaScript 错误处理机制

catch中抛出异常,用 throw e ,不要用 throw new Error(e) ,因为 e 本身就是一个 Error 对象了,具有错误的完整堆栈信息stack, new Error 会改变堆栈信息,将堆栈定位到当前这一行。

4.2 try…finally… 不能捕获错误

下面的代码,由于没有catch,错误会直接被window捕获。

1try {
2    throw new Error('出错啦啦啦')
3} finally {
4    console.log('啦啦啦')
5}

4.3 try…catch…只能捕获同步代码的错误,不能捕获异步代码错误

下面的代码,错误将不能被catch捕获。

1try {
2    setTimeout(() => {
3        throw new Error('出错啦!')
4    })
5} catch(e){
6    // 不会执行
7    console.dir(e)
8}

因为setTimeout是异步任务,里面回调函数会被放入到宏任务队列中,catch中代码块属于同步任务,处于当前的事件队列中,会立即执行。(参考js事件循环机制:https://yangbo5207.github.io/wutongluo/ji-chu-jin-jie-xi-lie/shi-er-3001-shi-jian-xun-huan-ji-zhi.html)
当setTimeout中回调执行时,try/catch中代码块已不在堆栈中。所以错误不能被捕获。

5 promise

Promise对象是JavaScript的一种异步操作解决方案。Promise是构造函数,也是对象。

Promise的三种状态:

  • pending 异步操作未完成

  • fulfilled 异步操作成功

  • rejected 异步操作失败  

如果一个promise没有resolve或reject,将一直处于pending状态。

5.1 Promise的两个方法

  • Promise.prototype.then 通常用来添加异步操作成功的回调

  • Promise.prototype.catch 用来添加异步操作失败的回调

5.2 Promise内部的错误捕获

用Promise可以解决“回调地狱”的问题,但如果不能好处理Promise错误,将会陷入另一个地狱:错误将被“吞掉”,可能不会在控制台打印,也不能被window捕获。给调试、线上故障排查带来很大困难。

promise内部抛出的错误, 都不会被window捕获, 除非用了setTimeout/setInterval。

为了证明我的结论,我举了一些例子:

例子1,错误会抛出到控制台,promise.catch回调能够执行,但错误不会被window捕获。

1p = new Promise(()=>{
2    throw new Error('栗子1')
3})
4
5p.catch((e) => {
6    console.dir(e)
7})

例子2,p.then中但回调函数出错,错误会抛出到控制台,promise.catch回调能够执行,但错误不会被window捕获。

1p = new Promise((resolve, reject) => {
2    resolve()
3})
4
5p.then(() => {
6    throw new Error('栗子2')
7}).catch((e) => {
8    console.dir(e)
9})

例子3,p.catch回调出错,错误会抛出到控制台,后续的promise.catch回调能够执行,但错误不会被window捕获。

1p = new Promise((resolve, reject) => {
2    reject()
3})
4
5p.catch(() => {
6    throw new Error('栗子2')
7}).catch((e) => {
8    console.dir(e)
9})

例子4,错误会抛出到控制台,后续的promise.catch回调不会执行,错误会被window捕获。

 1p = new Promise((resolve, reject) => {
 2    reject()
 3})
 4
 5p.catch(() => {
 6    setTimeout((e) => {
 7        throw new Error('栗子2')
 8    })
 9}).catch((e) => {
10    console.dir(e)
11})

例3和例4完全不一样的结果,为什么会这样呢?因为promise内部也实现了类似于try/catch的错误捕获机制,能够捕获错误。

参考promise 实现:https://github.com/then/promise/blob/master/src/core.js

 1// es6实现的promise部分源码
 2function Promise(fn) {
 3  ...
 4  doResolve(fn, this);
 5}
 6
 7function doResolve(fn, promise) {
 8  var done = false;
 9  var res = tryCallTwo(fn, function (value) {
10   ...
11  }, function (reason) {
12   ...
13  });
14}
15
16function tryCallTwo(fn, a, b) {
17  try {
18    fn(a, b);
19  } catch (ex) {
20    LAST_ERROR = ex;
21    return IS_ERROR;
22  }
23}

从es6实现的promise可以发现, Promise() promise.then() promise.catch() 回调函数执行时,都会被放到try…catch…中执行, 所以错误不能被 window.onerror 捕获。而try…catch…包括setTimeout/setInterval 等异步代码时,是不能捕获到错误的。

5.3 在全局捕获promise错误

5.3.1 unhandledrejection 捕获未处理Promise错误

用法:

 1window.addEventListener('error', (e) => {
 2    console.log('window error', e)
 3}, true)
 4
 5window.addEventListener('unhandledrejection', (e) => {
 6    console.log('unhandledrejection', e)
 7});
 8
 9let p = function() {
10    return new Promise((resolve, reject) => {
11        reject('出错啦')
12    })
13}
14
15p()
深入理解 JavaScript 错误处理机制

兼容性  :

深入理解 JavaScript 错误处理机制

unhandledrejection事件在浏览器中兼容性不好,通常不这么做。

6 async/await

当调用一个 async 函数时,会返回一个 Promise 对象。当这个 async 函数返回一个值时,Promise 的 resolve 方法会负责传递这个值;当 async 函数抛出异常时,Promise 的 reject 方法也会传递这个异常值。

async/await的用途是简化使用 promises 异步调用的操作,并对一组 Promises执行某些操作。正如Promises类似于结构化回调,async/await类似于组合生成器和 promises。

async 函数的返回值会被隐式的传递给 Promise.resolve

async函数内部的错误处理

async的推荐用法:

1async function getInfo1() {
2  try {
3    await ajax();
4  } catch (e) {
5    // 错误处理
6    throw e
7  }
8}

await后面函数返回的promise的状态有三种:

  • pending 异步操作未完成

  • fulfilled 异步操作成功

  • rejected 异步操作失败  

async函数主体处理结果如下:

1)fulfilled 异步操作成功  
如果await后面函数返回的promise的状态是fulfilled(成功),那程序将会继续执行await后面到代码。下面的例子都是fulfilled状态的。

 1# demo 1: ajax success, no ajax().catch
 2async function getInfo1() {
 3  try {
 4    await ajax();
 5    console.log('123')
 6  } catch (e) {
 7    // 错误处理
 8    throw e
 9  }
10}
11
12# demo 2:  ajax failed, ajax().catch do nothing
13async function getInfo1() {
14  try {
15    await ajax().catch(e => do nothing)
16    console.log('123')
17  } catch (e) {
18    // 错误处理
19    throw e
20  }
21}

2)rejected 异步操作失败  
如果await后面函数返回的promise的状态是rejected(失败),那程序将不会执行await后面的代码,而是转到 catch 中到代码块。下面的例子都是fulfilled状态的。

 1# demo 1: ajax failed
 2async function getInfo1() {
 3  try {
 4    // ajax failed
 5    await ajax();
 6    console.log('123')
 7  } catch (e) {
 8    // 错误处理
 9    throw e
10  }
11}
12
13# demo 2:  ajax failed, ajax().catch throw error
14async function getInfo1() {
15  try {
16     // ajax failed
17    await ajax().catch(error => throw error)
18    console.log('123')
19  } catch (e) {
20    // 错误处理
21    throw e
22  }
23}

3)pending 异步操作未完成  
如果await后面函数 ajax 没有被 resolvereject ,那么将 ajax 一直处于pending状态,程序将不会往后执行await 后面代码,也不能被catch捕获,async函数也将一直处于pending状态。
这样的代码在我们身边很常见,举一个我遇到过的例子。

 1function initBridge() {
 2    return new Promise((resolve, reject) => {
 3        window.$ljBridge.ready((bridge, webStatus) => {
 4            ...
 5            resolve()
 6        })
 7    })
 8}
 9
10function async init(){
11    try{
12        await initBradge()
13        // do something
14    } catch(e) {
15        throw e
16    }
17}
18
19init()

上面的代码,initBradge由于没有被正确当reject,当出错时,将一直处于pending状态。init内部即不能捕获错误,也不能继续往后执行,将一直处于pending状态。

7 参考链接

1) 错误处理机制

(https://wangdoc.com/javascript/features/error.html)

2)js事件循环机制

(https://yangbo5207.github.io/wutongluo/ji-chu-jin-jie-xi-lie/shi-er-3001-shi-jian-xun-huan-ji-zhi.html)

作   者: 包龙星 (企业代号名)

出品人:漠北鹰、CC老师(企业代号名)

---------- END ----------

推荐阅读

BEM命名法

体验的升华——WEEX

深入理解 JavaScript 错误处理机制


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数据科学家养成手册

数据科学家养成手册

高扬 / 电子工业出版社 / 2017-5 / 79

作为认知科学的延伸,数据科学一方面应该越来越引起广大大数据工作者的重视,另一方面也要撩开自己的神秘面纱,以最为亲民的姿态和每位大数据工作者成为亲密无间的战友,为用科学的思维方式进行工作做好理论准备。《数据科学家养成手册》从众多先贤及科学家的轶事讲起,以逐步归纳和递进的脉络总结出科学及数据科学所应关注的要点,然后在生产的各个环节中对这些要点逐一进行讨论与落实,从更高、更广的视角回看科学及数据科学在各......一起来看看 《数据科学家养成手册》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具