简体   繁体   English

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

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

Just wanted to preemptively say that I am familiar with async/await and promises in JavaScript so no need to link me to some MDN pages for that.只是想先发制人地说我熟悉 JavaScript 中的 async/await 和 promises,所以不需要为此链接我到一些 MDN 页面。

I have a function to fetch user details and display it on the UI.我有一个 function 来获取用户详细信息并将其显示在 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)
  }
}

It first makes HTTP call via someHttpCall and if it succeeds then it proceeds to fetchUserDetails and it that succeeds as well then we display the details on Ui via returndisplayUserDetails .它首先通过 someHttpCall 调用someHttpCall ,如果成功则继续fetchUserDetails并且它也成功然后我们通过returndisplayUserDetails在 Ui 上显示详细信息。

If someHttpCall failed, we will stop and not make fetchUserDetails call.如果someHttpCall失败,我们将停止并且不进行fetchUserDetails调用。 In other words, we want to separate the error handling for someHttpCall and it's data handling from fetchUserDetails换句话说,我们希望将someHttpCall的错误处理和它的数据处理与fetchUserDetails

The function I wrote is with nested try catch blocks which doesn't scale well if the nesting becomes deep and I was trying to rewrite it for better readability using plain then and catch我写的 function 带有嵌套的try catch块,如果嵌套变深,则无法很好地扩展,我试图使用 plain thencatch重写它以获得更好的可读性

This was my first atttempt这是我的第一次尝试

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

The problem with this is that the second then will run ie displayUserDetails even with someHttpCall failing.问题在于,即使someHttpCall失败,第二个也会运行, then displayUserDetails To avoid this I had to make the previous .catch blocks throw为了避免这种情况,我不得不让之前的.catch块抛出

so this is the updated version所以这是更新版本

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

However now the second catch will get called as a result of the throw.然而,现在第二次接球将因投掷而被调用。 So when the someHttpCall failed, after we handled the someHttpCallError error, we would enter this block (fetchUserDetailsError) => { console.log('fetching user error', fetchUserDetailsError) } which is not good since fetchUserDetails never gets called so we shouldn't need to handle fetchUserDetailsError (I know someHttpCallError became fetchUserDetailsError in this case)所以当someHttpCall失败时,在我们处理了someHttpCallError错误之后,我们会进入这个块(fetchUserDetailsError) => { console.log('fetching user error', fetchUserDetailsError) }这不好,因为fetchUserDetails永远不会被调用所以我们应该' t 需要处理fetchUserDetailsError (我知道someHttpCallError在这种情况下变成fetchUserDetailsError

I can add some conditional checks in there to distinguish the two errors but it seems less ideal.我可以在其中添加一些条件检查来区分这两个错误,但这似乎不太理想。 So I am wondering how I can improve this by using .then and .catch to achieve the same goal here.所以我想知道如何通过使用.then.catch来实现相同的目标。

I can add some conditional checks in there to distinguish the two errors but it seems less ideal.我可以在其中添加一些条件检查来区分这两个错误,但这似乎不太理想。

Actually, that sounds like an ideal situation.实际上,这听起来像是一个理想的情况。 That means that you don't have to nest any try / catch blocks which could make you code a lot more readable.这意味着您不必嵌套任何可以使您的代码更具可读性的try / catch块。 This is one of the things that async / await is meant to solve.这是async / await旨在解决的问题之一。

A solution could be is to create custom errors by extending the Error inteface to be able to determine what and where the error occurs.一种解决方案可能是通过扩展Error接口来创建自定义错误,以便能够确定错误发生的内容和位置。

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

Throw your errors within the functions that correspond with the error.在与错误对应的函数中抛出错误。

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

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

Now you can control your error flow by checking the name property on the error to differentiate your errors.现在,您可以通过检查错误上的name属性来控制错误流,以区分您的错误。

async function fetchUser() {
  try {
    const user = await someHttpCall()
    const details = await fetchUserDetails(user)
    returndisplayUserDetails(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我想知道如何通过使用.then.catch在这里实现相同的目标来改进这一点

You don't get to avoid the nesting if you want to replicate the same behaviour:如果要复制相同的行为,则无法避免嵌套:

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

(The exact equivalent to try / catch would use .then(…).catch(…) instead of .then(…, …) , but you might not actually want that .) (与try / catch完全等价的将使用.then(…).catch(…)而不是 .then( .then(…, …) ,但您可能实际上并不想要那个。)

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 […]我写的 function 是 [嵌套的],如果嵌套变深,它就不能很好地扩展,我试图重写它以获得更好的可读性 […]

For that, I would recommend to combine await with .catch() :为此,我建议将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);
  }
}

(The cause option for Error is still quite new, you might need a polyfill for that) Errorcause选项仍然很新,你可能需要一个polyfill)

I've been inspired by Rust 's Result type, so this is how I do it (comments included below):我受到RustResult类型的启发,所以我就是这样做的(评论包括在下面):

TS Playground TS游乐场

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).如果您不熟悉 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
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM