来聊一聊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( |
哪怕在用户点击这个按钮之前,你的页面 js 已经报错了。但这个在用户触发按钮的时候还是会执行这个 click 回调的哦。
一个典型的例子如下:
1 | var a = 1; |
设置一个间隔定时器,每隔 3s 都会打印一下数字 a,并抛出一个错误。错误会导致后面的 console 无法执行。但是每隔 3s 钟 setInterval 函数还是会被触发(在浏览器中是可以再次触发的,但在 Node.js 中会导致进程退出无法再次触发)。
所以异常抛出会阻止后方同步代码的执行(可以理解为执行栈碰到错误,就会清空执行栈),于是 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)
这样的机制—通过一个自定义 err 对象参数来告诉 Node.js 应用出异常了。即,他没有真的 throw error,所以哪怕你不做任何异常处理,也不会导致应用挂掉。所以 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
2
3
4
5<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") |
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 和异常处理的演进