JavaScript:async/await 和 then/catch 异步错误处理的区别

JavaScript: differences between async error handling with async/await and then/catch

只是想先发制人地说我熟悉 async/await 和 JavaScript 中的承诺,所以不需要 link 我去一些 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 并且它也成功然后我们通过 [=18 在 Ui 上显示详细信息=].

如果someHttpCall失败,我们将停止而不进行fetchUserDetails调用。换句话说,我们想将 someHttpCall 的错误处理和 fetchUserDetails

的数据处理分开

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

这是我的第一次尝试

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

问题在于第二个 then 将 运行 即 displayUserDetails 即使 someHttpCall 失败。为了避免这种情况,我不得不让之前的 .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 来实现相同的目标来改进这一点。

I can add some conditional checks in there to distinguish the two errors but it seems less ideal.

实际上,这听起来很理想。这意味着您不必嵌套任何 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
    }
  }
}

I am wondering how I can improve this by using .then and .catch to achieve the same goal here

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

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(…) instead of .then(…, …), but 。)

The function I wrote is [nested] which doesn't scale well if the nesting becomes deep and I was trying to rewrite it for better readability […]

为此,我建议将 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)

我受到了 Rust's Result 类型的启发(它迫使你处理一路上的每一个潜在错误)。

所以我所做的是在每个单独的函数中处理异常,并且绝不允许抛出异常,而是 return 抛出错误(如果出现问题)或所需的 return 值(如果没有异常发生)。这是我如何做的一个例子(包括评论):

TS Playground

If you aren't familiar with TypeScript, you can see the JavaScript-only version of the following code (with no type information) at the TypeScript Playground link above (on the right side of the page).

// 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
}