0%

JavaScript中Async/Await的错误处理

传统的错误处理方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function thisThrows() {
throw new Error("Thrown from thisThrows()");
}

try {
thisThrows();
} catch (e) {
console.error(e);
} finally {
console.log('We do cleanup here');
}

// Output:
// Error: Thrown from thisThrows()
// ...stacktrace
// We do cleanup here

非常简单,不过多赘述

一个包装在Async中的Try Catch

现在我们修改这个程序,把thisThrows()函数标记为async。此时抛出的错误,实际上相当于抛出一个Reject。一个Async函数,总是返回一个Promise

  • 当没有定义返回语句的时候,函数运行结束后实际返回的是Promise,相当于return Promise.Resolve()
  • 当有定义返回语句的时候,相当于返回了一个带有值的Promise,相当于return Promise.Resolve('My return String')
  • 当抛出错误的时候,相当于return Promise.Reject(error)

看下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function thisThrows() {
throw new Error("Thrown from thisThrows()");
}

try {
thisThrows();
} catch (e) {
console.error(e);
} finally {
console.log('We do cleanup here');
}

// output:
// We do cleanup here
// UnhandledPromiseRejectionWarning: Error: Thrown from thisThrows()

thisThrows返回一个Reject,所以我们使用常规的try...catch无法正常的捕捉到错误。

thisThrws标记为async,所以我们调用的时候,代码不会等待,finally块会先执行,所以这里无法捕捉到错误。

有两个方式可以解决这个问题:

第一个解决方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function thisThrows() {
throw new Error("Thrown from thisThrows()");
}

async function run() {
try {
await thisThrows();
} catch (e) {
console.error(e);
} finally {
console.log('We do cleanup here');
}
}

run();

// Output:
// Error: Thrown from thisThrows()
// ...stacktrace
// We do cleanup here

第二个解决方式

1
2
3
4
5
6
7
8
9
10
11
12
async function thisThrows() {
throw new Error("Thrown from thisThrows()");
}

thisThrows()
.catch(console.error)
.then(() => console.log('We do cleanup here'));

// Output:
// Error: Thrown from thisThrows()
// ...stacktrace
// We do cleanup here

async/await的方式相对来说更容易理解。

注意点

从async函数中返回

考虑下下面代码会输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async function thisThrows() {
throw new Error("Thrown from thisThrows()");
}

async function myFunctionThatCatches() {
try {
return thisThrows();
} catch (e) {
console.error(e);
} finally {
console.log('We do cleanup here');
}
return "Nothing found";
}

async function run() {
const myValue = await myFunctionThatCatches();
console.log(myValue);
}

run();

我们可能期待输出

1
2
We do cleanup here
Nothing Found

实际输出一个UnhandledPromiseRejection

我们分析下

  • thisThrows() 是异步方法;
  • 异步方法中抛出了一个错误,实际返回的是Promise.Reject
  • myFunctionThatCatches中返回了这个Promise.Reject
  • 外部是以await标记的,发现是一个Reject的Prmoise,所以抛出unlandled promise rejection

我们可以在返回中增加await解决这个问题(第七行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async function thisThrows() {
throw new Error("Thrown from thisThrows()");
}

async function myFunctionThatCatches() {
try {
return await thisThrows(); // <-- Notice we added here the "await" keyword.
} catch (e) {
console.error(e);
} finally {
console.log('We do cleanup here');
}
return "Nothing found";
}

async function run() {
const myValue = await myFunctionThatCatches();
console.log(myValue);
}

run();

// Outptut:
// Error: Thrown from thisThrows()
// ...stacktrace
// We do cleanup here
// Nothing found

重置stack trace

在代码中,经常会看到有人捕获错误并将其包装在一个新的错误中,就像下面的代码片段中一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function thisThrows() {
throw new Error("Thrown from thisThrows()");
}

function myFunctionThatCatches() {
try {
return thisThrows();
} catch (e) {
throw new TypeError(e.message);
} finally {
console.log('We do cleanup here');
}
}

async function run() {
try {
await myFunctionThatCatches();
} catch (e) {
console.error(e);
}
}

run();

// Outputs:
// We do cleanup here
// TypeError: Error: Thrown from thisThrows()
// at myFunctionThatCatches (/repo/error_stacktrace_1.js:9:15) <-- Error points to our try catch block
// at run (/repo/error_stacktrace_1.js:17:15)
// at Object.<anonymous> (/repo/error_stacktrace_1.js:23:1)

注意我们的堆栈跟踪仅从我们捕获原始异常的地方开始。当我们在 2 行创建错误并在 9 行捕获它时,我们会丢失原始的堆栈跟踪,因为我们现在创建了一个新的 TypeError 类型的错误,只保留原始的错误消息(有时我们甚至都不保留)。

如果 thisThrows() 函数中有更多的逻辑,在该函数的某个地方抛出了一个错误,我们在记录的堆栈跟踪中看不到问题的起源,因为我们创建了一个新的错误,它将生成一个全新的堆栈跟踪。如果我们只是重新抛出原始错误,我们就不会遇到这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function thisThrows() {
throw new Error("Thrown from thisThrows()");
}

function myFunctionThatCatches() {
try {
return thisThrows();
} catch (e) {
// Maybe do something else here first.
throw e;
} finally {
console.log('We do cleanup here');
}
}

async function run() {
try {
await myFunctionThatCatches();
} catch (e) {
console.error(e);
}
}

run();

// Outputs:
// We do cleanup here
// Error: Thrown from thisThrows()
// at thisThrows (/repo/error_stacktrace_2.js:2:11) <-- Notice we now point to the origin of the actual error
// at myFunctionThatCatches (/repo/error_stacktrace_2.js:7:16)
// at run (/repo/error_stacktrace_2.js:18:15)
// at Object.<anonymous> (/repo/error_stacktrace_2.js:24:1)

堆栈跟踪现在指向实际错误的起源,即我们脚本的第 2 行。

处理错误时要意识到这个问题是很重要的。有时这可能是可取的,但通常这会掩盖问题的来源,使得调试问题的根源变得困难。如果你为包装错误创建自定义错误,请确保跟踪原始的堆栈跟踪,以免调试变成一场噩梦。

总结

  • 我们可以使用 try...catch 来处理同步代码。
  • 我们可以使用 try...catch (与 async 函数结合使用)和 .catch() 方法来处理异步代码的错误。
  • try 块中返回一个promise时,如果你希望 try...catch 块捕获错误,请确保 await 它。
  • 在包装错误并重新抛出时,请注意,您会丢失带有错误来源的堆栈跟踪。