来聊一聊JavaScript中的异常捕获
同步代码中的异常
我们先从简单的同步代码,来了解JavaScript中的异常是如何捕获和处理的
如何抛出异常
语法异常或使用了语言中缺少的功能
1
2
3
4
5
6
7function test (a) {
console.log(1)
// 这里会抛出异常
var b = a.slice(a)
console.log(2)
}
test(1)上面代码中
console.log(2)
会无法得到执行,因为a.slice抛出异常并报错了主动抛出异常
1
2
3
4
5
6
7function test() {
console.log(1)
// 主动抛出异常
throw new Error('throw error');
console.log(2)
}
test(1)这里会主动抛出一个异常,也会导致console.log(2)无法得到执行
异常去哪了
由于上面所举的例子中,我们的异常都没有捕获,因此会在全局抛出。浏览器控制台会看到 Uncaught Error
的报错并可以展开堆栈信息。
捕获同步异常
如果想捕获同步代码中产生的异常,只需按照传统编程语言的思路,使用try catch即可解决
1 | try { |
这里会有如下输入:
1 | before throw error |
其中第二句是捕获到异常后主动打印出来的错误信息。
异步代码中的异常
下面这种代码是异步的
1 | setTimeout(function () { |
异步代码的回调里产生的异常是无法被try catch捕获的
1 | try { |
而,在异步回调执行的时候try catch,是可以捕获异常的(因为对于局部的代码来说是同步的)
1 | try { |
异常是否会阻止代码继续执行?
在没有catch异常的情况下,异常抛出地方的后续代码肯定无法得到执行。
但是新的事件回调触发后,js还会执行吗? 比如你页面上某个按钮绑定了click事件:
1 | document.querySelector('#testbtn').addEventListener('click', function () { |
哪怕在用户点击这个按钮之前,你的页面js已经报错了。但这个在用户触发按钮的时候还是会执行这个click回调的哦。
一个典型的例子如下:
1 | var a = 1; |
设置一个间隔定时器,每隔3s都会打印一下数字a,并抛出一个错误。错误会导致后面的console无法执行。但是每隔3s钟setInterval函数还是会被触发(在浏览器中是可以再次触发的,但在 Node.js 中会导致进程退出无法再次触发)。
所以异常抛出会阻止后方同步代码的执行,但不会阻止异步任务和它的回调的执行(特指在浏览器中)。
wnidow.error 捕获未catch的异常
有没有办法监听到异步代码函数内的异常呢?
浏览器有一个机制: 不管同步代码还是异步代码,只要发生了异常且没有被捕获,那么会一直传递到全局抛出。所以我们可以在全局监听整个 JavaScript 运行过程的异常. window.onerror 就是做这个事情的。
看下面的代码:
1 | /** |
通过监听 window 的 error 事件 就能监听任何异常了。 (注意:如果你自己try catch捕获住了异常,并且没有继续向上抛出(即: 你吃掉了异常),那么全局是无法捕获到异常的)
注意我这里讲的是 监听
,并不是 捕获
。 因为 window.onerror 事实上 跟 try…catch 还是有很大的区别。window.error 只能在全局去监听到你代码内未捕获的异常(可以用作上报),但无法像 catch 一样 吃掉
这个异常。 可以说, window.error 发现错误的时候就代表这个错误已经抛到了全局,此时业务代码已经被异常中断了,此时已经为时已晚无法再做处理了。
Node.js 的异常处理
趁热打铁,我们来看看 Node.js 中的异常。上面说了那么多浏览器中 JavaScript 异常处理。实际上在 Node.js 中也是类似的,但是有个明显的区别是:
在浏览器中如果没有捕获异常,异常抛到全局,会被浏览器发现但不会导致浏览器进程退出; 然而 Node.js 来说,如果异常没有被
吃掉
,被抛到全局的异常会直接导致进程退出。
例如我们写一个 web server 的例子(也可以用setTimeout来做例子):
1 | const http = require('http') |
这段代码,只要来一个请求就会导致 web server 挂掉。原因就在于异步的异常无法捕获。
在 Node.js(也包括浏览器端的一些API) 的底层 API 中, 为了防止异步IO时抛出直接抛出异常导致 Node.js 应用挂掉,一般这些 API 都会采用 事件
或 callback(err, data)
这样的机制来告诉 Node.js 应用出异常了。哪怕你看不做任何异常处理,也不会导致应用挂掉。所以 Node.js 中的代码一般是这样写的:
1 | fs.mkdir('/dir', function (e) { |
但实际上 Node.js 应用会肥非常复杂,不能排除自己业务代码中抛出的同步或异步的异常。在 Node.js 中也有类似于浏览器的全局捕获机制,Node.js 可以监听到进程内的全局所有异常做一些日志记录等工作
1 | process.on('uncaughtException', function (e) { |
跨源异常捕获
对于window.onerror方式的异常捕获方式,无法捕获跨源脚本爆出的错误。跨源的脚本报错,window.onerror只能收到一个 Script Error
的errorMessage信息,无法获取行号、错误对象等信息。
如果希望过滤掉跨域的错误,可以这样写代码:
1 | window.onerror = function (msg, url, lineNo, columnNo, error) { |
要想可以捕获跨源的脚本错误,可以这样设置:
添加script的crossorigin属性
1
<script type="text/javascript" src="//xxx.domain.com/error.js" crossorigin></script>
配置一下服务器,设置静态资源Javascript的Response为Access-Control-Allow-Origin
1
2header('Access-Control-Allow-Origin:*');
header('Content-type:text/javascript');
其实这里就是跨源资源共享的跨域机制了。
eval的异常
在使用localStorage或者动态加载远程js脚本的时候,通常采用的策略是: “用XMLHTTPRequest”把远程脚本请求回来,再把脚本内容放入 eval
函数执行,这会导致eval会把字符串内容放在eval自身所在位置的上下文里执行。
但eval执行代码有个问题,就是: 脚本中的错误异常会抛出,但不会被全局的window.onerror捕获. 因此,对于eval执行脚本的错误,如果希望被全局window.onerror函数捕获,则需要自己trycatch并自己抛出.
示例如下:
1 | try { |
Promise中的异常
Promise改造了异步代码的回调书写方式,变成了then函数来注册回调的方式。
1 | var p = Promise.resolve(5) |
Promise中的异常,可以通过传入一个reject回调来捕获:
1 | var p = Promise.resolve(5) |
由于Promise的错误如果没有被处理,则会一直往后面的Promise传递,所以你可以在链式调用的最后面去catch这个Promise错误:
1 | sendRequest('test.html').then(function(data1) { |
Promise 内报错或异常都会触发reject
在 Promise 内部如果抛出了错误,或代码语法错误,相当于会触发Promise的reject过程。
1 | var promise = new Promise(function(resolve, reject) { |
但是throw出来的异常错误会导致后面的代码 console.log
无法执行;而如果是手动 Promise.reject()
的异常,Promise内部的后方代码还会执行。例如打印”我是 error 之后的代码”
不catchPromise的错误会怎样?
如果不去捕获Promise的错误(例如reject调用,或者Promise内部发生了异常或语法错误),那么chrome浏览器会在控制台抛出这个错误,而据说safari和firefox没有任何反应。
这是个例子:
1 | var promise = new Promise(function (resolve, reject) { |
异步之后的非reject异常无法捕获
1 | var promise = new Promise(function(resolve, reject) { |
这种异常只能被抛到外层window, try catch也无法捕获,Promise也无法捕获. 注意: 这里这个异常是非Promise异常了,可以被widow.onerror捕获
因此,如果要在Promise里抛出异常,最好使用reject的方式。reject无论在同步代码里还是在异步代码里都能让这个Promise抛出异常且能被后面的catch接住。
1 | Promise.resolve() |
全局捕获Promise异常
对于在Promise内部直接throw Error 或者 在 Promise内部异步代码回调里reject抛异常,这两种方式都是Promise异常。
promise最有争议的地方就是当一个promise失败但是没有rejection handler处理错误时静默失败。不过浏览器和Node.js都有相应的处理机制,两者大同小异,都是通过事件的方式监听. 有两个全局事件可以用来监听 Promise 异常:
- unhandledRejection:当promise失败(rejected),但又没有处理时触发,event handler 有2个参数: reason,promise;
- rejectionHandled: 当promise失败(rejected),被处理时触发,hanler 有1个参数: promise;
generator 的异常
generator 又是一个比较复杂的东东啦。它提供了一个 throw 方法可以向函数内抛出一个异常。
1 | function *gen() { |
上述代码会输出:
1 | { value: 'a', done: false } |
由于第一次执行next后,函数执行会暂停在 yield a
这一句。 虽然a已经返回,但这一句还没有执行完,等下次调用 next 或 throw 时才会继续执行完成 赋值
才标志着本句结束。由于第二次执行时并不是 next, 而是一个 throw, 所以yield b 无法得到执行,然后catch后面已经没有其他代码,所以本次 throw 会返回 { value: undefined, done: true }
, 表示generator已经执行完毕。
其实 generator 跟异步没有关系,抛开执行流程的特殊性,就把他当做普通函数来看就好了。这种函数体内捕获错误的机制,大大方便了对错误的处理。多个yield表达式,可以只用一个try…catch代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在 Generator 函数内部写一次catch语句就可以了。
除了函数体外触发异常被函数体内捕获之外, 函数体内触发的异常也可以被函数体外捕获。
1 | function* foo() { |
一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。
为什么throw error可以导致后面代码无法执行?
在 JavaScript 函数中,只有 return / yield / throw 会中断函数的执行,其他的都无法阻止其运行到结束的
浏览器全局优雅地处理异常
对于在代码中强制开发者处理好自己的异常是不太客观的,因此可以在全局去监听异常(包括让开发者主动抛出的异常),然后做出相应的处理。对于需要提示用户的就提示用户,对于意外的异常,可以打印或上报。贴一个网上的例子:
1 | import EnsureError from './ensureError.js'; |
几个错误类的实现在这里:
1 | function EnsureError(message = 'Default Message') { |
总结
看文本文你学会JavaScript里的异常了吗。可以做做这个Promise的题目试试: https://www.zhihu.com/question/39780175
refer
用window.onerror捕获并上报Js错误
初探Promise
Callback Promise Generator Async-Await 和异常处理的演进