Skip to content

Commit 8af572d

Browse files
committed
209
1 parent 0c542af commit 8af572d

2 files changed

Lines changed: 257 additions & 1 deletion

File tree

readme.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
前端界的好文精读,每周更新!
88

9-
最新精读:<a href="./前沿技术/208.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204.4%E3%80%8B.md">208.精读《Typescript 4.4》</a>
9+
最新精读:<a href="./前沿技术/209.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8D%95%E8%8E%B7%E6%89%80%E6%9C%89%E5%BC%82%E6%AD%A5%20error%E3%80%8B.md">209.精读《捕获所有异步 error》</a>
1010

1111
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
1212

@@ -166,6 +166,7 @@
166166
- <a href="./前沿技术/206.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%80%E7%A7%8D%20Hooks%20%E6%95%B0%E6%8D%AE%E6%B5%81%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88%E3%80%8B.md">206.精读《一种 Hooks 数据流管理方案》</a>
167167
- <a href="./前沿技术/207.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%20infer%20%E5%85%B3%E9%94%AE%E5%AD%97%E3%80%8B.md">207.精读《Typescript infer 关键字》</a>
168168
- <a href="./前沿技术/208.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204.4%E3%80%8B.md">208.精读《Typescript 4.4》</a>
169+
- <a href="./前沿技术/209.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8D%95%E8%8E%B7%E6%89%80%E6%9C%89%E5%BC%82%E6%AD%A5%20error%E3%80%8B.md">209.精读《捕获所有异步 error》</a>
169170

170171
### 设计模式
171172

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
成熟的产品都有较高的稳定性要求,仅前端就要做大量监控、错误上报,后端更是如此,一个未考虑的异常可能导致数据错误、服务雪崩、内存溢出等等问题,轻则每天焦头烂额的处理异常,重则引发线上故障。
2+
3+
假设代码逻辑没有错误,那么剩下的就是异常错误了。
4+
5+
由于任何服务、代码都可能存在外部调用,只要外部调用存在不确定性,代码就可能出现异常,所以捕获异常是一个非常重要的基本功。
6+
7+
所以本周就精读 [How to avoid uncaught async errors in Javascript](https://advancedweb.hu/how-to-avoid-uncaught-async-errors-in-javascript/) 这篇文章,看看 JS 如何捕获异步异常错误。
8+
9+
## 概述
10+
11+
之所以要关注异步异常,是因为捕获同步异常非常简单:
12+
13+
```typescript
14+
try {
15+
;(() => {
16+
throw new Error('err')
17+
})()
18+
} catch (e) {
19+
console.log(e) // caught
20+
}
21+
```
22+
23+
但异步错误却无法被直接捕获,这不太直观:
24+
25+
```typescript
26+
try {
27+
;(async () => {
28+
throw new Error('err') // uncaught
29+
})()
30+
} catch (e) {
31+
console.log(e)
32+
}
33+
```
34+
35+
原因是异步代码并不在 `try catch` 上下文中执行,唯一的同步逻辑只有创建一个异步函数,所以异步函数内的错误无法被捕获。
36+
37+
要捕获 `async` 函数内的异常,可以调用 `.catch`,因为 `async` 函数返回一个 Promise:
38+
39+
```typescript
40+
;(async () => {
41+
throw new Error('err')
42+
})().catch((e) => {
43+
console.log(e) // caught
44+
})
45+
```
46+
47+
当然也可以在函数体内直接用 `try catch`
48+
49+
```typescript
50+
;(async () => {
51+
try {
52+
throw new Error('err')
53+
} catch (e) {
54+
console.log(e) // caught
55+
}
56+
})()
57+
```
58+
59+
类似的,如果在循环体里捕获异常,则要使用 `Promise.all`
60+
61+
```typescript
62+
try {
63+
await Promise.all(
64+
[1, 2, 3].map(async () => {
65+
throw new Error('err')
66+
})
67+
)
68+
} catch (e) {
69+
console.log(e) // caught
70+
}
71+
```
72+
73+
也就是说 `await` 修饰的 Promise 内抛出的异常,可以被 `try catch` 捕获。
74+
75+
但不是说写了 `await` 就一定能捕获到异常,一种情况是 Promise 内再包含一个异步:
76+
77+
```typescript
78+
new Promise(() => {
79+
setTimeout(() => {
80+
throw new Error('err') // uncaught
81+
}, 0)
82+
}).catch((e) => {
83+
console.log(e)
84+
})
85+
```
86+
87+
这个情况要用 `reject` 方式抛出异常才能被捕获:
88+
89+
```typescript
90+
new Promise((res, rej) => {
91+
setTimeout(() => {
92+
rej('err') // caught
93+
}, 0)
94+
}).catch((e) => {
95+
console.log(e)
96+
})
97+
```
98+
99+
另一种情况是,这个 `await` 没有被执行到:
100+
101+
```typescript
102+
const wait = (ms) => new Promise((res) => setTimeout(res, ms))
103+
104+
;(async () => {
105+
try {
106+
const p1 = wait(3000).then(() => {
107+
throw new Error('err')
108+
}) // uncaught
109+
await wait(2000).then(() => {
110+
throw new Error('err2')
111+
}) // caught
112+
await p1
113+
} catch (e) {
114+
console.log(e)
115+
}
116+
})()
117+
```
118+
119+
`p1` 等待 3s 后抛出异常,但因为 2s 后抛出了 `err2` 异常,中断了代码执行,所以 `await p1` 不会被执行到,导致这个异常不会被 catch 住。
120+
121+
而且有意思的是,如果换一个场景,提前执行了 `p1`,等 1s 后再 `await p1`,那异常就从无法捕获变成可以捕获了,这样浏览器会怎么处理?
122+
123+
```typescript
124+
const wait = (ms) => new Promise((res) => setTimeout(res, ms))
125+
126+
;(async () => {
127+
try {
128+
const p1 = wait(1000).then(() => {
129+
throw new Error('err')
130+
})
131+
await wait(2000)
132+
await p1
133+
} catch (e) {
134+
console.log(e)
135+
}
136+
})()
137+
```
138+
139+
结论是浏览器 1s 后会抛出一个未捕获异常,但再过 1s 这个未捕获异常就消失了,变成了捕获的异常。
140+
141+
这个行为很奇怪,当程序复杂时很难排查,因为并行的 Promise 建议用 Promise.all 处理:
142+
143+
```typescript
144+
await Promise.all([
145+
wait(1000).then(() => {
146+
throw new Error('err')
147+
}), // p1
148+
wait(2000),
149+
])
150+
```
151+
152+
另外 Promise 的错误会随着 Promise 链传递,因此建议把 Promise 内多次异步行为改写为多条链的模式,在最后 `catch` 住错误。
153+
154+
还是之前的例子,Promise 无法捕获内部的异步错误:
155+
156+
```typescript
157+
new Promise((res, rej) => {
158+
setTimeout(() => {
159+
throw Error('err')
160+
}, 1000) // 1
161+
}).catch((error) => {
162+
console.log(error)
163+
})
164+
```
165+
166+
但如果写成 Promise Chain,就可以捕获了:
167+
168+
```typescript
169+
new Promise((res, rej) => {
170+
setTimeout(res, 1000) // 1
171+
})
172+
.then((res, rej) => {
173+
throw Error('err')
174+
})
175+
.catch((error) => {
176+
console.log(error)
177+
})
178+
```
179+
180+
原因是,用 Promise Chain 代替了内部多次异步嵌套,这样多个异步行为会被拆解为对应 Promise Chain 的同步行为,Promise 就可以捕获啦。
181+
182+
最后,DOM 事件监听内抛出的错误都无法被捕获:
183+
184+
```typescript
185+
document.querySelector('button').addEventListener('click', async () => {
186+
throw new Error('err') // uncaught
187+
})
188+
```
189+
190+
同步也一样:
191+
192+
```typescript
193+
document.querySelector('button').addEventListener('click', () => {
194+
throw new Error('err') // uncaught
195+
})
196+
```
197+
198+
只能通过函数体内 `try catch` 来捕获。
199+
200+
## 精读
201+
202+
我们开篇提到了要监控所有异常,仅通过 `try catch``then` 捕获同步、异步错误还是不够的,因为这些是局部错误捕获手段,当我们无法保证所有代码都处理了异常时,需要进行全局异常监控,一般有两种方法:
203+
204+
- `window.addEventListener('error')`
205+
- `window.addEventListener('unhandledrejection')`
206+
207+
`error` 可以监听所有同步、异步的运行时错误,但无法监听语法、接口、资源加载错误。而 `unhandledrejection` 可以监听到 Promise 中抛出的,未被 `.catch` 捕获的错误。
208+
209+
在具体的前端框架中,也可以通过框架提供的错误监听方案解决部分问题,比如 React 的 [Error Boundaries](https://reactjs.org/docs/error-boundaries.html)、Vue 的 [error handler](https://v3.vuejs.org/api/application-config.html#errorhandler),一个是 UI 组件级别的,一个是全局的。
210+
211+
回过头来看,本身 js 提供的 `try catch` 错误捕获是非常有效的,之所以会遇到无法捕获错误的经常,大多是因为异步导致的。
212+
213+
然而大部分异步错误,都可以通过 `await` 的方式解决,我们唯一要注意的是,`await` 仅支持一层,或者说一条链的错误监听,比如这个例子是可以监听到错误的:
214+
215+
```typescript
216+
try {
217+
await func1()
218+
} catch (err) {
219+
// caught
220+
}
221+
222+
async function func1() {
223+
await func2()
224+
}
225+
226+
async function func2() {
227+
throw Error('error')
228+
}
229+
```
230+
231+
也就是说,只要这一条链内都被 `await` 住了,那么最外层的 `try catch` 就能捕获异步错误。但如果有一层异步又脱离了 `await`,那么就无法捕获了:
232+
233+
```typescript
234+
async function func2() {
235+
setTimeout(() => {
236+
throw Error('error') // uncaught
237+
})
238+
}
239+
```
240+
241+
针对这个问题,原文也提供了例如 `Promise.all`、链式 Promise、`.catch` 等方法解决,因此只要编写代码时注意对异步的处理,就可以用 `try catch` 捕获这些异步错误。
242+
243+
## 总结
244+
245+
关于异步错误的处理,如果还有其它未考虑到的情况,欢迎留言补充。
246+
247+
> 讨论地址是:[精读《捕获所有异步 error》· Issue #350 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/350)
248+
249+
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
250+
251+
> 关注 **前端精读微信公众号**
252+
253+
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
254+
255+
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)

0 commit comments

Comments
 (0)