36

深入理解 JavaScript 错误处理机制

 5 years ago
source link: https://mp.weixin.qq.com/s/-1Q6BjHt8F2IDavPo8pD6w?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Ur6Vr2j.gif

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

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)

vEjAjii.png!web

控制台输出

上面的例子中, 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依然在执行"

3mmMn2u.png!web

点击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}

aiuqqaF.png!web

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()
iUj6jiJ.jpg!web

兼容性  :

QRz6B3f.jpg!web

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

fEV3imE.gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK