如何优雅的处理异步错误
目录
- 异步 IIFE
- 异步 forEach
- Promise
- DOM 事件监听
一、异步 IIFE
一个简单的异步 IIFE
(async () => {
throw new Error("err");
})();
先看看同步函数如何与try..catch块一起工作
try {
(() => {
throw new Error("err");
})();
}catch(e) {
console.log(e); // caught
}
将其更改为异步函数
try {
(async () => {
throw new Error("err");
})();
}catch(e) {
console.log(e) // uncaught
}
在这种情况下,catch 不会运行,这是为什么?
在同步情况下,错误是同步错误,因此同步的 try..catch 可以处理。
但是异步函数的工作方式不同,那里执行的同步操作会创建一个新的 Promise 并且滞后运行。
程序在抛出错误时离开,因此 try..catch 无法处理它。
解决方案
使用 catch 函数来处理错误
(async () => {
throw new Error("err");
})().catch((e) => {
console.log(e); // caught
});
或者在 async 函数中添加 try..catch
(async () => {
try {
throw new Error("err");
}catch(e) {
console.log(e); // caught
}
})();
二、异步 forEach
用 try..catch 处理同步 forEach 中的错误
try{
[1,2,3].forEach(() => {
throw new Error("err");
});
}catch(e) {
console.log(e); // caught
}
但是若操作为异步就会出现异常
try{
[1,2,3].forEach(async () => {
throw new Error("err");
});
}catch(e) {
console.log(e)
}
// 结果:引发 3 个“未捕获”的异常
Uncaught (in promise) Error: err
Uncaught (in promise) Error: err
Uncaught (in promise) Error: err
可以使用 await Promise.all 解决:
try{
await Promise.all([1,2,3].map(async () => {
throw new Error("err");
}));
}catch(e) {
console.log(e); // caught
}
三、promise
简单的 primise 捕获异常
new Promise(() => {
throw new Error("err");
}).catch((e) => {
console.log(e); // caught
});
Promise.resolve().then(() => {
throw new Error("err");
}).catch((e) => {
console.log(e); // caught
})
但是如果 promise 内部有其他的异步处理将无法被捕获
new Promise(() => {
setTimeout(() => {
throw new Error("err"); // uncaught
}, 0);
}).catch((e) => {
console.log(e);
});
这个情况要用 reject 方式抛出异常才能被捕获
new Promise((res, rej) => {
setTimeout(() => {
rej('err') // caught
}, )
}).catch((e) => {
console.log(e)
})
另一种情况是多个 await 中报错执行的问题
const wait = (ms) => new Promise((res) => setTimeout(res, ms));
(async () => {
try{
const p1 = wait(3000).then(() => {throw new Error("err")}); // uncaught
await wait(2000).then(() => {throw new Error("err2")}); // caught
await p1;
}catch(e) {
console.log(e);
}
})();
结果是只能捕获到 err2
Error: err2
Uncaught (in promise) Error: err
原因是 p1 等待 3s 后抛出异常,但 2s 后抛出了 err2 异常,中断了代码执行,
所以 await p1 不会被执行到,导致这个异常不会被 catch。
若当 p1 改成 1s 后
const wait = (ms) => new Promise((res) => setTimeout(res, ms));
(async () => {
try{
const p1 = wait(1000).then(() => {throw new Error("err")});
await wait(2000);
await p1;
}catch(e) {
console.log(e);
}
})();
结果是 1s 后会抛出一个未捕获异常,
但再过 1s 这个未捕获异常就消失了,变成了捕获的异常!
这个行为相当奇怪,因此并行的 Promise 建议用 Promise.all 处理:
await Promise.all([
wait(1000).then(() => {
throw new Error('err')
}),
wait(2000),
])
这会处理这两个错误,即使只会抛出第一个错误。
另外 Promise 的错误会随着 Promise 链传递,
因此建议把 Promise 内多次异步行为改写为多条链的模式,在最后 catch 中错误。
new Promise((res, rej) => {
setTimeout(() => {
throw Error('err')
}, 1000)
}).catch((error) => {
console.log(error) // uncaught
})
// 改写成这样即可捕获
new Promise((res, rej) => {
setTimeout(res, 1000)
})
.then((res, rej) => {
throw Error('err')
})
.catch((error) => {
console.log(error) // caught
})
原因是用 Promise 链式代替了内部多次异步嵌套,
这样多个异步行为会被拆解为对应 Promise 链式的同步行为,
Promise 就可以捕获啦。
四、DOM 事件监听
DOM 事件监听内抛出的错误都无法被捕获
document.querySelector("button").addEventListener("click", async () => {
throw new Error("err"); // uncaught
});
document.querySelector("button").addEventListener("click", () => {
throw new Error("err"); // uncaught
})