其实async函数的错误机制和一般的错误非常像
- 当一个async函数中出现未捕捉的promise rejecrt,或一般的错误,都会自动中断,并向上抛出reject
比如这里runPromise函数中没有捕捉错误,此时错误会继续向上传播,并能在test处被捕获
test()
async function test() {
try{
await runPromise()
}catch(err){
console.log('catch')
}
}
async function runPromise() {
await rejectPromise();
// 运行会在这里中断,并向上抛出reject
console.log('done');
}
async function rejectPromise() {
return Promise.reject('error')
}
或者是发生一般的错误,也会向上抛出reject,并最后在test处被捕获
test()
async function test() {
try{
await runPromise()
}catch(err){
console.log('catch')
}
}
async function runPromise() {
await errorPromise();
// 运行会在这里中断,并向上抛出reject
console.log('done');
}
async function errorPromise(){
JSON.parse('aaa') // 这里会抛出异常,并抛出reject
}
- 若async函数中捕获了错误,则不会再继续向上传播错误
比如下面例子,test函数中将捕获不到错误
test()
async function test() {
try{
await runPromise()
}catch(err){
console.log('catch')
}
}
async function runPromise() {
try{
await rejectPromise();
// 运行会在这里中断,但不会向上抛出reject
console.log('done');
}catch(err){
console.log('reject')
}
}
async function rejectPromise() {
return Promise.reject('error')
}
- 若调用async函数时,没有使用await,则不会捕获到错误,错误也不会向上传播
下面例子中,runPromise运行不会中断,test也不会捕获到错误
test()
async function test() {
try{
await runPromise()
}catch(err){
console.log('catch')
}
}
async function runPromise() {
rejectPromise();
console.log('done');
}
async function rejectPromise() {
return Promise.reject('error')
}
最佳实践
对于被调函数而言:
- 如果我们希望将错误交给调用者解决,则不应该使用try/catch,让js自动抛出错误
- 若我们需要处理错误,则必须在catch处手动返回reject或抛出错误,否则调用者无法捕捉
- 调用async函数时一定要使用await,否则错误无法传播
优雅地调用async
若我们不希望代码中出现UnhandledPromiseRejectionWarning的错误时,我们需要对每一个async函数都包一个try/catch,有什么办法可以优化它
这个例子是一个点击事件:点击删除按钮后调用删除接口,若成功则给出成功的提示,失败则给出失败的提示
async function onDeleteBtnClick(id) {
try{
await deleteSomethingAPI(id)
console.log('删除成功')
}catch(err){
console.log('删除失败')
}
}
这个方法有两点不好的地方:
- 业务逻辑和错误处理的逻辑放在一起,使得用户需要同时关注两种情况的分支
- 事件函数不应该包含具体的业务逻辑,这样会导致事件与具体的业务耦合,不利于业务逻辑的复用和测试
第一步我们要做的是:分离业务逻辑和错误处理逻辑
我们进行以下修改,deleteSomething只关心成功时的业务逻辑,错误处理交由onDeleteBtnClick处理
async function onDeleteBtnClick(id) {
try{
await deleteSomething(id)
}catch(err){
console.log('删除失败')
}
}
async function deleteSomething(id){
await deleteSomethingAPI(id)
console.log('删除成功')
}
此时onDeleteBtnClick可以优化为这样,于是我们消灭了try/catch
function onDeleteBtnClick(id) {
deleteSomething(id).catch(err => {
console.log('删除失败')
})
}
进一步优化: 使用专门的函数来捕捉错误
我们可以将onDeleteBtnClick的try/catch抽出,处理成一个专门处理错误的函数,并将错误交由统一的errorHandler处理
async function promiseRunner(promise){
let arg = Array.prototype.slice.call(arguments,1)
try{
return await promise(...arg)
}catch(err){
return errorHandler(err)
}
}
function errorHandler(err){
console.log(err)
}
针对上面的例子,可以写一个能自动给出提示信息的函数来运行async函数,比如这样
import { Message } from 'element-ui';
async function promiseRunnerWithMessage(promise) {
try{
let arg = Array.prototype.slice.call(arguments,1)
const data = await promise(...arg);
Message.success("操作成功");
return data;
}catch(err){
Message.error('操作失败')
return errorHandler(err)
}
}
最后onDeleteBtnClick可以优化成这样
而且也发现,事件函数和具体业务完成了解耦
function onDeleteBtnClick(id) {
promiseRunnerWithMessage(deleteSomethingAPI,id)
}
总结
- 将方法中的业务逻辑与错误处理逻辑分离,有利于对两种情况分别进行处理
- 使用专门的方法进行错误逻辑的处理,把注意力专注于业务逻辑上
补充:
可以发现promiseRunner并没有返回类似标志位的东西,意味着外层无法判断promiseRunner的运行情况。但这就是我想要的效果。若外层还需要对标志位进行判断,则说明错误分支和业务分支又再次纠缠在一起。因此promiseRunner应该作为最顶层的函数调用,后续不应包含任何逻辑