对中国开发者最具吸引力的科技企业有哪些?快来为你 pick 的企业投票! 了解详情
写点什么

深入理解 javascript 错误处理机制

2019 年 9 月 26 日

深入理解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:33364

评论

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

渴望提升自己技术能力的程序员的必备宝典!这份在阿里内部被封神的《Java技术成长笔记》真的太牛了!

Java成神之路

Java 程序员 架构 面试 编程语言

阿里云容器服务入选云原生边缘「领导力企业TOP3」,推动「原生云边」基础设施标准建立

阿里巴巴云原生

阿里云 容器 开发者 云原生 边缘计算

佛萨奇Forsage系统开发,智能合约dapp技术

薇電13242772558

智能合约 dapp

等保数据备份和恢复关键点,这些你该知道!

华为云开发者社区

数据 容灾 恢复

LeetCode题解:127. 单词接龙,双向BFS,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

Mysql中,1=1和 1=1=1 和 -1=-1 和 -1=-1=-1 和 5=5 和 5=5=5 有什么区别

Geek_de9857

MySQL sql 返回值 1=1=1 -1=1=1

一文详解激活函数

书豪

原创 | 使用JPA实现DDD持久化-O/R映射元数据-值属性映射

编程道与术

Java hibernate 编程 mybatis jpa

“深入内核,拒绝蒙圈”,阿里巴巴一位P7级架构师总结整理的这份《Java架构成长笔记》彻底火了。

Java成神之路

Java 程序员 架构 面试 编程语言

涨薪神作!华为内部操作系统与网络协议笔记爆火,这也太香了吧

Java成神之路

Java 程序员 架构 面试 编程语言

有了这份“Java神级面试资料”,奉劝各位耗子尾汁赶紧扔掉你在网上找的那些千篇一律的面试题

Java成神之路

Java 程序员 架构 面试 编程语言

985研究生熬夜23天吃透845页架构宝典 终收割腾讯Java岗offer!再也不用怀疑人生了

比伯

Java 编程 架构 面试 程序人生

forsage以太坊矩阵系统软件开发|forsage以太坊矩阵APP开发

开發I852946OIIO

系统开发

架构师 3 期 3 班 -week4- 作业

zbest

作业 week4

几款Java开发者必备常用的工具,准点下班不在话下

华为云开发者社区

Java 工具 开发

阿里华为等大厂如何处理数值精度/舍入/溢出问题

Java架构师迁哥

eCharts -- 如何修改柱状图中相关数据的顺序?如何在鼠标悬浮时增加百分比信息?如何为柱状图设置分组?

Geek_de9857

柱状图 eCharts 修改数据顺序 百分比提示信息 设置分组

架构师训练营第 13 周学习总结

netspecial

极客大学架构师训练营

90分钟10个手写案例,从源码底层给你讲解7种线程池创建方式

996小迁

Java 源码 架构 资料 笔记

flink 使用curl,通过RESTful api,上传和删除jar包

Geek_de9857

flink RESTful curl 上传jar 删除jar

从面试角度分析LinkedList源码

Java旅途

Java List 集合 linkedlist

女朋友突然问我DNS是个啥....

乱敲代码

计算机网络 DNS DNS服务器

扩招1W人,字节跳动内部公开12月份Java岗71道面试题

比伯

Java 编程 架构 面试 程序人生

OpenKruise v0.7.0 版本发布:新增周期任务分发控制器

阿里巴巴云原生

阿里云 容器 开发者 运维 云原生

滴滴DoKit阶段性成果汇报之一机多控

工具 滴滴开源 DoKit

程序员因重复记录日志撑爆ELK被辞退!

Java架构师迁哥

Alibaba最新《Java架构核心宝典》限时开放下载,互联网主流技术详解总结,提升技术能力的必备宝典!

Java成神之路

Java 程序员 架构 面试 编程语言

【涂鸦物联网足迹】用煲仔饭来说明IaaS/PaaS/SaaS的区别

IoT云工坊

云计算 IaaS PaaS SaaS 云平台

Hive中,同时存在map、array、struct这三种格式,应如何在建表语句中指定分隔符?

Geek_de9857

hive struct map array 分隔符

15年华为云视频架构师采访实录:揭秘未来音视频行业的科技趋势!

华为云开发者社区

直播 视频 华为云

JavaScript中,if判断未生效的一些特殊情况

Geek_de9857

js 1 if 0 不生效

滴滴 Logi 日志管理与分析平台

滴滴 Logi 日志管理与分析平台

深入理解javascript错误处理机制-InfoQ