我只是想先发制人地说,我熟悉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
块,如果嵌套变得很深,它就不能很好地扩展,我试图使用普通的then
和catch
重写它以获得更好的可读性
这是我第一次参加
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);
}
}
(Error
的cause
选项仍然很新,您可能需要一个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
}