同步错误,try catch就可以捕获。
而异步任务的错误,无论是setTimeout 还是promise.then,单独的try catch都无法捕获。
// 并不会被catch所捕获,程序直接报错function func() {try {setTimeout(() => {throw new Error('opps! error')}, 1000)} catch(e) {console.log(e, 'err')}}func();
因为事件循环的机制,在执行try catch时,遇到setTimeout,视为异步任务并调用WebApis,定时器线程(另外的线程,由宿主环境提供)开始工作,1000毫秒后,将setTimeout的callback加入到任务队列中(注意,是在另外的线程中等待1000毫秒过去然后将setTimeout的callback加入到任务队列中,而不是直接将setTimeout加入到任务队列中)。当前的try catch执行完毕,执行栈退出的时刻,开始在任务队列中读取队列中的callback,同时形成callback相应的执行栈。由于try catch 的执行栈已经退出,catch是无法拿到任务队列中的callback执行栈里抛出的错误的。
同理,promise.then中的错误也是无法try catch的,因为promise.then属于微任务,将在当前执行栈为空后,才去任务队列中查找微任务,微任务形成自己的执行栈,try-catch执行栈早就退出了,所以无法捕获。
一. promise的异常捕获
1.构造函数中
function main1() {try {new Promise(() => {throw new Error('promise1 error')})} catch(e) {console.log(e.message);}}function main2() {try {Promise.reject('promise2 error');} catch(e) {console.log(e.message);}}// 以上两个方法里的try catch都不能捕获error,因为promise内部的错误不会冒泡出来,直接被promise吞掉了,只能被promise.catch才可以捕获。即使是在catch中再throw error,也可以被后续的catch所捕获。
2.then 中的异常捕获
function main3() {Promise.resolve(true).then(() => {try {throw new Error('then');} catch(e) {return e;}}).then(e => console.log(e.message));}// 可以在回调函数内部进行try-catch,并将error返回,这时的error会传递给下一个then中。// 或者在then中直接throw error(或者reject),并在catch中捕获function main4() {Promise.resolve(true).then(() => {throw new Error('then');}).catch(e => console.log(e.message));}
tip: 当promise的实例resolve后,错误无法被捕获
var promise=new Promise(function(resolve,reject){resolve('hi');throw new Error('test');//该错误无法被捕获})promise.then(function(e){console.log('then ---', e)}).catch(function(e){console.log('error ---', e)})// catch无法捕获,也不会有报错// 因为promise内部的error都被promise吞掉了,所以无法冒泡到外层。// 但是promise一旦resolve或者reject了,状态就不会再改变,所以throw error 也无法改变promise已经被resolve (fullfilled)的事实,即不会有效果// 注意!!!在resolve或者reject后的代码依旧会继续执行的,所以要根据实际情况选择resolve/reject的位置,或者加return
总结:
- promise内部的错误(reject、throw)被promise内部处理,可被catch捕获。
- promise一旦状态完成,不会再改变。resolve后再throw error没有作用。
- resolve/reject后的代码仍然会被执行。
二. async/await 异常捕获
async函数可以保留运行堆栈
const fetchFailure = () => new Promise((resolve, reject) => {setTimeout(() => {// 模拟请求if(1) reject('fetch failure...');})})async function main () {try {const res = await fetchFailure();console.log(res, 'res');} catch(e) {console.log(e, 'e.message');}}main();// 当promise中出现异常,会被promise捕获,然后调用generator的throw方法,抛出错误,而async方法可以吧保留运行堆栈,错误被catch捕获。// async函数可以保留运行堆栈const a = async () => {await b();c();};// 上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c()报错,错误堆栈将包括a()。
async的实现原理
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {// ...}// 等同于function fn(args) {return spawn(function* () {// ...});}
所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。
下面给出spawn函数的实现,基本就是前文自动执行器的翻版。
function spawn(genF) {return new Promise(function(resolve, reject) { // 返回一个promiseconst gen = genF(); // 生成器函数,可能调用它的next或者throw方法function step(nextF) { // 不断next,直到结束let next;try {next = nextF();} catch(e) {return reject(e);}if(next.done) {return resolve(next.value);}// 这里的next.value,即被await的promise请求等异步函数,如果这个promise失败,则进入then的失败回调中,抛错Promise.resolve(next.value).then(function(v) {step(function() { return gen.next(v); });}, function(e) {step(function() { return gen.throw(e); }); // Generator.prototype.throw()把错误抛出来,才能被外层的try-catch捕获到});}step(function() { return gen.next(undefined); });});}
总结:
- async返回的是promise,可以被then、catch。async函数本身是个异步的。
- async内部可以使用try-catch捕获异常。
- async内部,遇到await则暂停执行,控制权交给async函数的父级函数,等异步返回后,重新获取控制权,继续执行下方代码。
其他知识点:
宏任务、微任务
在任务队列这里,还有一个小的区分。异步任务分为宏任务(macro task)和微任务(micro task),并且会添加到不同的任务队列中。
tasks
不过查阅 html 规范中,并没有 macro task 的定义(我是看别人文章都这么写),为了严谨性,下面都称为 tasks。 一个事件循环中可能会有一个或多个 tasks,这个是根据 task 源划分的,比如事件(鼠标单击、键盘操作)就是一个 task 源,可能会放到一个任务队列中,XHR 的回调放到一个队列中,但是具体的优先级,这么多 tasks 到底先从哪个取,这个浏览器会根据情况去获取以达到更好的交互体验,先不放到本次研究范围,大概了解 tasks 会按照源去分成多个就好了。 常见的宏任务 tasks 包括:
- XMLHttpRequest 回调
- 事件回调(onClick)
- setTimeout/setInterval
- history.back
宏任务会被加入到下一个事件循环的队列的任务队列中。
micro-task
microtask queue 在每个事件循环中只有一个,跟 tasks 区分,它的本意是尽可能早的执行异步任务。常见的 microtask 包括:
- Promise.then
- MutationObserver
- Object.observe
- process.nextTick(node 中)
Promise.then 在不同的平台实现方式不同,不过大多数都参照 promise/A+ 规范,是当做 microtask 处理的。
microtask 是在一个事件循环结束后(执行栈为空时),立即执行,即 tasks 的一个任务执行后,并且会清空 microtask 队列。另外,如果 microtask 中新添加了 microtask,会放到 queue 末尾一起执行。
当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,执行完毕后,再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。
yum install telnetapk add telnetapt-get install telnet各个系统自取安装方式
