JavaScript:async/await和then/catch的异步错误处理之间的差异



我只是想先发制人地说,我熟悉JavaScript中的async/await和promise,所以不需要为此将我链接到一些MDN页面。

我有一个获取用户详细信息并在UI上显示的功能。


async function someHttpCall() {
throw 'someHttpCall error'
}
async function fetchUserDetails() {
throw 'fetchUserDetails error'
}
function displayUserDetails(userDetails) {
console.log('userDetails:', userDetails)
}
async function fetchUser() {
try {
const user = await someHttpCall()
try {
const details = await fetchUserDetails(user)
returndisplayUserDetails(details)
} catch (fetchUserDetailsError) {
console.log('fetching user error', fetchUserDetailsError)
}
} catch (someHttpCallError) {
console.log('networking error:', someHttpCallError)
}
}

它首先通过someHttpCall进行HTTP调用,如果成功,则前进到fetchUserDetails,如果也成功,则我们通过returndisplayUserDetails显示Ui的详细信息。

如果someHttpCall失败,我们将停止并不调用fetchUserDetails。换句话说,我们希望将someHttpCall的错误处理及其数据处理与fetchUserDetails分开

我写的函数是嵌套的try catch块,如果嵌套变得很深,它就不能很好地扩展,我试图使用普通的thencatch重写它以获得更好的可读性

这是我第一次参加

function fetchUser2() {
someHttpCall()
.then(
(user) => fetchUserDetails(user),
(someHttpCallError) => {
console.log('networking error:', someHttpCallError)
}
)
.then(
(details) => {
displayUserDetails(details)
}, //
(fetchUserDetailsError) => {
console.log('fetching user error', fetchUserDetailsError)
}
)
}

这样做的问题是,即使someHttpCall失败,第二个then也将运行,即displayUserDetails。为了避免这种情况,我不得不让之前的.catch块抛出

所以这是的更新版本

function fetchUser2() {
someHttpCall()
.then(
(user) => fetchUserDetails(user),
(someHttpCallError) => {
console.log('networking error:', someHttpCallError)
throw someHttpCallError
}
)
.then(
(details) => {
displayUserDetails(details)
}, //
(fetchUserDetailsError) => {
console.log('fetching user error', fetchUserDetailsError)
}
)
}

然而,现在第二次接球将被称为投掷的结果。因此,当someHttpCall失败时,在我们处理了someHttpCallError错误后,我们将进入这个块(fetchUserDetailsError) => { console.log('fetching user error', fetchUserDetailsError) },这是不好的,因为fetchUserDetails从未被调用,所以我们不需要处理fetchUserDetailsError(我知道在这种情况下someHttpCallError变成了fetchUserDetailsError(

我可以在其中添加一些条件检查来区分这两个错误,但这似乎不太理想。所以我想知道如何通过使用.then.catch来改进这一点,以实现相同的目标。

我想知道如何通过使用.then.catch来实现的相同目标

如果你想复制相同的行为,你就无法避免嵌套:

function fetchUser2() {
return someHttpCall().then(
(user) => {
return fetchUserDetails(user).then(
(details) => {
return displayUserDetails(details)
},
(fetchUserDetailsError) => {
console.log('fetching user error', fetchUserDetailsError)
}
)
},
(someHttpCallError) => {
console.log('networking error:', someHttpCallError)
throw someHttpCallError
}
)
}

(与try/catch完全等效的是使用.then(…).catch(…)而不是.then(…, …),但您可能实际上并不想要。(

我写的函数是嵌套的,如果嵌套变得很深,它就不能很好地扩展,我试图重写它以获得更好的可读性[…]

为此,我建议将await.catch():结合使用

async function fetchUser() {
try {
const user = await someHttpCall().catch(someHttpCallError => {
throw new Error('networking error', {cause: someHttpCallError});
});
const details = await fetchUserDetails(user).catch(fetchUserDetailsError => {
throw new Error('fetching user error', {cause: fetchUserDetailsError});
});
return displayUserDetails(details);
} catch (someError) {
console.log(someError.message, someError.cause);
}
}

(Errorcause选项仍然很新,您可能需要一个polyfill(

我可以在其中添加一些条件检查来区分这两个错误,但这似乎不太理想。

实际上,这听起来是一个理想的情况。这意味着您不必嵌套任何try / catch块,这可以使您的代码可读性更强。这就是async / await要解决的问题之一。

一种解决方案是通过扩展Error接口来创建自定义错误,从而能够确定错误发生的方式和位置。

class CustomError extends Error {
constructor(name, ...args) {
super(...args)
this.name = name
}
}

将您的错误放入与错误对应的函数中。

async function someHttpCall() {
throw new CustomError('HttpCallError', 'someHttpCall error');
}
async function fetchUserDetails(user) {
throw new CustomError('UserDetailsError', 'fetchUserDetails error')
}

现在,您可以通过检查错误上的name属性来区分错误,从而控制错误流。

async function fetchUser() {
try {
const user = await someHttpCall()
const details = await fetchUserDetails(user)
return displayUserDetails(details)
} catch (error) {
switch(error.name) {
case 'HttpCallError':
console.log('Networking error:', error)
break
case 'UserDetailsError':
console.log('Fetching user error', error)
break
}
}
}

我受到了Rust的Result类型的启发(它迫使您处理过程中的每一个潜在错误(。

因此,我所做的是处理每个单独函数中的异常,从不允许抛出异常,而是返回Error(如果出现问题(或所需的返回值(如果没有发生异常(。下面是我如何做到的一个例子(包括评论(:

TS游乐场

如果您不熟悉TypeScript,您可以在上面(页面右侧(的TypeScript Playground链接中看到以下代码的纯JavaScript版本(没有类型信息(。

// This is the code in my exception-handling utility module:
// exception-utils.ts
export type Result <T = void, E extends Error = Error> = T | E;
export function getError (value: unknown): Error {
return value instanceof Error ? value : new Error(String(value));
}
export function isError <T>(value: T): value is T & Error {
return value instanceof Error;
}
export function assertNotError <T>(value: T): asserts value is Exclude<T, Error> {
if (value instanceof Error) throw value;
}
// This is how to use it:
// main.ts
import {assertNotError, getError, isError, type Result} from './exception-utils.ts';
/** 
* Returns either Error or string ID,
* but won't throw because it catches exceptions internally
*/
declare function getStringFromAPI1 (): Promise<Result<string>>;
/** 
* Requires ID from API1. Returns either Error or final number value,
* but won't throw because it catches exceptions internally
*/
declare function getNumberFromAPI2 (id: string): Promise<Result<number>>;
/**
* Create version of second function with no parameter required:
* Returns either Error or final number value,
* but won't throw because it catches exceptions internally
* 
* The previous two functions work just like this, using the utilities
*/
async function fetchValueFromAPI2 (): Promise<Result<number>> {
try {
const id = await getStringFromAPI1(); // Error or string
assertNotError(id); // throws if `id` is an Error
return getNumberFromAPI2(id); // Error or number
}
catch (ex) {
return getError(ex);
}
}
async function doSomethingWithValueFromAPI2 (): Promise<void> {
const value = await fetchValueFromAPI2(); // value is number or Error
if (isError(value)) {
// handle error
}
else console.log(value); // value is number at this point
}

最新更新