Linux 之父出席、干货分享、圆桌讨论,精彩尽在 OpenCloudOS 社区开放日,报名戳 了解详情
写点什么

深入理解 javascript 错误处理机制

  • 2019 年 9 月 26 日
  • 本文字数:5634 字

    阅读完需:约 18 分钟

深入理解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)
复制代码



控制台输出


上面的例子中,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 = 12342// Uncaught TypeError: a.concat is not a function3a.concat(9) 
复制代码


3)RangeError 范围错误


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


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


4)ReferenceError 引用错误


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


1// Uncaught ReferenceError: mmm is not defined2console.log(mmm)
复制代码


5)EvalError eval 错误


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


1// Uncaught TypeError: eval is not a constructor2new eval()3// 不会报错4eval = () => {}
复制代码


6)URIError URL 错误


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


1// URIError: URI malformed2    at decodeURIComponent 3decode4decodeURIComponent('%')
复制代码


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: 发生错误当脚本URL10* lineno: 发生错误当行号11* colno: 发生错误当列号12* error: Error对象13**/14window.onerror = function(message, source, lineno, colno, error) { ... }1516// 捕获vue中的错误, 重写console.error17console.error = () => {}
复制代码


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


3throw

MDN 关于 throw 的定义

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


MDN 上关于 throw 的定义,翻译得不够准确,对于“程序将会终止”,我有不同的看法,下面请听我的分析。


“throw 之后的语句将不会执行。”,这句话比较容易理解,例如:


1console.log(1)2throw 12343// 下面这行代码不会执行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  }1112  // 每1s打印一次13  setInterval(() => {14    log('setInterval依然在执行')15  }, 1000)1617  throw new Error('手动抛出异常')1819  // 这段代码不会执行20  document.getElementById('btn-2').onclick = function() {21    log(2)22  }23</script>
复制代码


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



点击 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 e6}
复制代码



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})45p.catch((e) => {6    console.dir(e)7})
复制代码


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


1p = new Promise((resolve, reject) => {2    resolve()3})45p.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})45p.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}1516function 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}1415p()
复制代码



兼容性 :



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 e7  }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}1112# demo 2:  ajax failed, ajax().catch do nothing13async function getInfo1() {14  try {15    await ajax().catch(e => do nothing)16    console.log('123')17  } catch (e) {18    // 错误处理19    throw e20  }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 e10  }11}1213# demo 2:  ajax failed, ajax().catch throw error14async function getInfo1() {15  try {16     // ajax failed17    await ajax().catch(error => throw error)18    console.log('123')19  } catch (e) {20    // 错误处理21    throw e22  }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} 910function async init(){11    try{12        await initBradge()13        // do something14    } catch(e) {15        throw e16    }17}1819init()
复制代码


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


作者介绍:


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


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


原文链接:


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


2019 年 9 月 26 日 16:33669

评论

发布
暂无评论
发现更多内容

ssh常用命令总结

入门小站

Linux

【Zookeeper技术专题】从Paxo算法出发认识一下Zookeeper

浩宇天尚

PAXOS ZooKeeper原理 paxos协议 10月日更 Paxo

最短路径算法

Dobbykim

算法 图论

每天学习使用代码片段(八)

devpoint

JavaScrip 10月日更

一篇文章带你了解Android 最新Camera框架

小驰笔记

android 音视频 camera

在线图片水平/垂直均等切割工具

入门小站

工具

计算架构模式之接口篇

十二万伏特皮卡丘

漫游语音识别技术——带你走进语音识别技术的世界

攻城先森

深度学习 音视频 nlp 语音识别

SpringMVC源码分析-HandlerAdapter(6)-ModelFactory组件分析

Brave

源码 springmvc 10月日更

如何激励员工?

石云升

项目管理 管理 引航计划 内容合集 10月日更

网络流量分析场景浅谈

穿过生命散发芬芳

后端 引航计划 网络流量分析

3. 有点难~ Python函数式编程中 itertools 模块

梦想橡皮擦

10月日更

【LeetCode】最长回文子串Java题解

HQ数字卡

算法 LeetCode 10月日更

工业级高精度电磁流量计解决方案

不脱发的程序猿

ADI 工业高精度传感器 流量传感器 优秀论文期刊

005云原生之Service Mesh(Istio+Envoy)

穿过生命散发芬芳

云原生 10月日更

在线随机抛硬币工具

入门小站

工具

阿里开源的这个库,让 Excel 导出不再复杂(简简单单的写)

看山

Java EasyExcel 10月日更

Scrum Patterns:Sprint回顾(译)

Bruce Talk

敏捷 译文 Agile Scrum Patterns

关心你的团队,这才是最有效的管理技巧

俞凡

管理 10月日更

一分钟搞懂FAST Agile

俞凡

敏捷 10月日更

网络架构知识总结

十二万伏特皮卡丘

谈 C++17 里的 Strategy 模式

hedzr

c++ 设计模式 策略模式 Design Patterns c++17

团队管理之如何成为核心员工

小诚信驿站

团队管理 管理 引航计划 内容合集

linux之autojump命令

入门小站

Linux

JavaScript 中的文档对象模型 DOM

devpoint

CSS html DOM 10月日更

容器 & 服务:Helm Charts(二)安装与使用

程序员架构进阶

Kubernetes 容器 Helm 10月日更 Helm Charts

【Flutter 专题】40 日常问题小结 (一)

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 10月日更

架构实战训练营|课后作业|模块5

Frode

「架构实战营」

Go dlv <autogenerate> 代码定位

非晓为骁

源码分析 Go 语言 dlv rt0_go autogenerate

SpringMVC源码分析-HandlerAdapter(5)-SessionAttributesHandler组件分析

Brave

源码 springmvc 10月日更

模块9

gevin

架构实战营

GPU容器虚拟化:用户态和内核态的技术和实践详解

GPU容器虚拟化:用户态和内核态的技术和实践详解

深入理解javascript错误处理机制_文化 & 方法_包龙星_InfoQ精选文章