先看一个典型的例子
function getList(){
return new Promise((rs,rj)=>{
rj('假装发生了错误')
})
}
try{
getList();
}
catch(e){
alert('遇到了些许错误哦~')
}
使用try...catch...捕获异常,发生错误时显示友好的提示。
emmm……可是实际会发生什么呢?
答案是,此处的异常将无法捕获到! 当getList()抛出异常后,嗯,流程就失控了。
为什么哪????我百思不得其解??
那就一步一步来研究研究,我看看是怎么个事!
Promise 和 async/await 的区别
1.语法层面的差别:
async/await提供了一种更接近同步代码的写法,使得异步代码的可读性和可维护性得到提高。在async函数中,如果遇到await表达式后面跟着的Promise被拒绝(rejected),它会抛出一个异常。
Promise本身是一个对象,它代表了一个异步操作的最终完成或失败。Promise的异常是通过.catch()方法或者.then()方法中的第二个参数函数来捕获的。
2.异常传播方式:
在async函数中,如果抛出异常,这个异常会向上冒泡,直到被捕获。这意味着你可以在async函数的外部使用try...catch来捕获异常。
对于Promise,你需要显式地使用.catch()或者在链式调用的.then()中处理异常,否则未捕获的异常会导致程序崩溃。
3.错误处理的位置:
使用async/await时,错误处理通常发生在try...catch块中,这使得错误处理的位置更加灵活,可以放在逻辑流程的任何地方。
使用Promise时,错误处理通常发生在链的末尾或者每个.then()调用之后,这可能会使得代码链变得冗长,尤其是在多个异步操作需要顺序执行时。
4.错误处理的链式调用:
在async/await中,你可以使用try...catch来捕获单个await表达式抛出的异常,也可以捕获整个async函数中的异常。
在Promise中,错误处理通常是链式进行的,每个.then()后面都可以跟随一个.catch()来处理前一个.then()中可能发生的错误。
5.错误处理的简洁性:
async/await因为其语法的简洁性,使得错误处理看起来更直观,更像同步代码。
Promise的错误处理可能需要更多的代码,尤其是在多个.then()调用链中,每个都需要单独的错误处理。
6.错误传播的透明度:
在async/await中,如果一个await表达式抛出异常,这个异常可以被async函数外部的try...catch捕获,这提高了错误传播的透明度。
在Promise中,如果链中的某个.then()没有正确处理异常,那么这个异常可能不会被立即捕获,直到链的末尾或者被显式地捕获。
在async函数中,如果抛出异常,这个异常会向上冒泡,直到被捕获。这意味着你可以在async函数的外部使用try...catch来捕获异常。
对于Promise,你需要显式地使用.catch()或者在链式调用的.then()中处理异常,否则未捕获的异常会导致程序崩溃。
Promise的异常捕获方式
打开控制台会发现,上面的例子将会抛出****Uncaught (in promise)异常。
这里要提到Promise的异常处理流程。
Promise创建时需要传入一个function,在这个function执行过程中,如果出现了异常则会对外抛出。 外部有2种方式来捕获这个异常:
方式一:catch()
let promise = new Promise(...);
promise.catch(e=>{
// TODO sth with e
})
方式二:async/await中的try...catch...
let promise = new Promise(...);
async function test(){
try{
await promise;
}
catch(e){
// TODO sth with e
}
}
如果异常既没有被方式一捕获又没有被方式二捕获,那么异常会被抛到全局中。
在NodeJs中可以通过process.on('unhandledRejection', e => {...})捕获全局异常
在浏览器中目前没有有效的方法可以捕获全局异常,所以我们要尽可能的去避免全局异常,全局异常通常会引起流程中断。
好了现在我们知道了如何捕获,那我就捕获一下吧:
function getList(){
return new Promise((rs,rj)=>{
rj('假装发生了错误')
})
}
async function main(){
try{
getList();
}
catch(e){
alert('遇到了些许错误哦~')
}
}
main();
欧吼!发现又有问题了,还是没有被捕获,这是为什么呢?
带着疑问继续往下⬇️
async/await 到底是什么?
简单来说,async/await 是以更舒适的方式使用 promise 的一种特殊语法,本质上就是promise。
让我们以 async 这个关键字开始。它可以被放置在一个函数前面,如下所示:
async function f() {
return 1;
}
在函数前面的 “async” 这个单词表达了一个简单的事情:即这个函数总是返回一个 promise。其他值将自动被包装在一个 resolved 的 promise 中。
上面的代码转化一下
// async function f() {
// return 1;
// }
// function getList(){
// return new Promise((rs,rj)=>{
// rj('假装发生了错误')
// })
// }
// async function main(){
// try{
// getList();
// }
// catch(e){
// alert('遇到了些许错误哦~')
// }
// }
// main();
async function f() {
return Promise.resolve(1);
}
function main(){
return Promise.resolve(XXXXXXX)
}
我们知道async表示这段代码是一个异步执行代码,在async中,await会阻碍后面代码的执行。如果没有await,代码就不会被阻塞,会以同步的方式进行。在js中,async和await是成对出现的,去思考一下,假如await单独出现会是什么情况?答案是后面的全部都会中断了,所以js中不允许单独出现。
这里给出一个await的例子
// 只在 async 函数内工作
let value = await promise;
关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。
这里的例子就是一个 1 秒后 resolve 的 promise:
async function f() {
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});
let result = await promise; // 等待,直到 promise resolve (*)
alert(result); // "done!"
}
f();
这个函数在执行的时候,“暂停”在了 (*) 那一行,并在 promise settle 时,拿到 result 作为结果继续往下执行。所以上面这段代码在一秒后显示 “done!”。
让我们强调一下:await 实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为 JavaScript 引擎可以同时处理其他任务:执行其他脚本,处理事件等。
相比于 promise.then,它只是获取 promise 的结果的一个更优雅的语法。并且也更易于读写。
不能在普通函数中使用 await
如果我们尝试在非 async 函数中使用 await,则会报语法错误:
function f() {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error
}
如果我们忘记在函数前面写 async 关键字,我们可能会得到一个这个错误。就像前面说的,await 只在 async 函数中有效。
未捕获异常的原因
例子中的Promise异常未被正常捕获,是因为promise虽然出现在try...catch...中,但是并没有被await,如此将不进入上述的异常捕获流程,一旦出现异常并且没有其它有效的catch时,就将抛出至全局。
new Promise(rs=>{
throw new Error('Error')
}).catch(e=>{
console.log('异常被捕获到了1')
})
new Promise(async rs=>{
throw new Error('Error')
}).catch(e=>{
console.log('异常被捕获到了2')
})
👆上面两个🌰可以被捕获吗?
根据Promise的链式调用的原则,来分析以上代码,第一个例子是Promise标准捕获异常的方法,所以第一个是可以被捕获到的。
第二个我们拆开来分析
new Promise(rs=>{
// async 相当于同步函数里又包了一层Promise
return new Promise(()=>{
// 内层Promise抛出异常
throw new Error('Error')
})
}).catch(e=>{ // 这里catch的是外层Promise
// 由于异常并未向上抛给外层Promise,所以此处catch不到
console.log('异常被捕获到了2')
})
new Promise(rs=>{
// async 相当于同步函数里又包了一层Promise
// Do sth
return new Promise(()=>{
// 内层Promise抛出异常
throw new Error('Error')
})
}).catch(e=>{ // 这里catch的是外层Promise
// 由于异常并未向上抛给外层Promise,所以此处catch不到
console.log('异常被捕获到了2')
})
链式的调用并不会像async/await一样会被统一处理,也不会层层上报,而是一旦未被捕获就马上会被上升为全局,接着整个流程就会被中断。
ok相信你已经明白了这两个的区别,下面我们接着讨论一下混用的情况。
async function getList(){
return new Promise((rs,rj)=>{
rj('假装发生了错误')
});
}
async function main(){
try{
await getList().catch(e=>{
console.log('异常捕获到了,位置1')
});
}
catch(e){
console.log('异常捕获到了,位置2')
}
}
main();
这种情况最后会打印出什么呢?
答案如图
还是用刚才的结论,await会等待代码的执行,getList首先返回一个Promise,状态为reject,用于链式调用原则reject状态会在相同代码层级中被.catch,await后面的代码全部执行完毕,阻塞结束,由于err已经被捕获所以位置2不会再被捕获了。
补充
1.try catch 只能捕获当前上下文中的错误,也就是只能捕获同步任务的情况,如下场景:
try {
throw "程序执行遇到了一些错误";
} catch(e) {
console.log(e)
}
// 控制台会输出:程序执行遇到了一些错误
对于异步的任务,try catch就显得无能为力,不能正确捕获错误:
try {
setTimeout(() => {
throw "程序执行遇到了一些错误"
})
} catch(e) {
console.log(e);
}
// 控制台输出:Uncaught 程序执行遇到了一些错误;
try {
Promise.reject('程序执行遇到了一些错误');
} catch(e) {
console.log(e);
}
// 控制台输出:Uncaught (in promise) 程序执行遇到了一些错误
2.then方法中的第二个参数和Promise.catch方法的区别
new Promise((resolve,reject) => {
setTimeout(() => {
resolve(1);
}, 1000)
}).then(res => {
console.log(res);
return new Promise((resolve,reject) => {
reject('第一个then方法报错了');
})
}, err => {
console.log(err);
}).then(res => {
console.log(res);
return new Promise((resolve,reject) => {
reject('第二个then方法报错了');
})
}, err => {
console.log(err) // 输出:'第一个then方法报错了'
}).catch(err => {
console.log(err); // 可以捕获整个promise调用链的错误
})
参考文献: