深入理解 javascript 错误处理机制

阅读数:64 2019 年 9 月 26 日 16:33

深入理解javascript错误处理机制

1 错误分类

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

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

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

Error 实例对象的三个属性:

  • message 错误提示信息
  • name 错误名称
    s tack 错误的堆栈

例如下面的代码,打印错误实例对象,可以得到 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 资源加载错误

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

复制代码
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, 会被覆盖, 导致错误没有正常上报。

3throw

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 之前的语句依旧在执行。

4try…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 中代码块已不在堆栈中。所以错误不能被捕获。

5promise

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 事件在浏览器中兼容性不好,通常不这么做。

6async/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 没有被 resolve 或 reject,那么将 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 状态。

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

本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。

原文链接:

https://mp.weixin.qq.com/s/-1Q6BjHt8F2IDavPo8pD6w

评论

发布