【问题标题】:Should an async API ever throw synchronously?异步 API 是否应该同步抛出?
【发布时间】:2014-03-20 05:14:33
【问题描述】:

我正在编写一个 JavaScript 函数,它发出一个 HTTP 请求并返回一个对结果的承诺(但这个问题同样适用于基于回调的实现)。

如果我立即知道为函数提供的参数无效,函数 throw 是否应该同步返回,或者它是否应该返回被拒绝的承诺(或者,如果您愿意,使用 Error 实例调用回调)?

异步函数应该始终以异步方式运行有多重要,尤其是在错误情况下?如果您知道程序不处于适合异步操作进行的状态,可以throw 吗?

例如:

function getUserById(userId, cb) {
  if (userId !== parseInt(userId)) {
    throw new Error('userId is not valid')
  }

  // make async call
}

// OR...

function getUserById(userId, cb) {
  if (userId !== parseInt(userId)) {
    return cb(new Error('userId is not valid'))
  }

  // make async call
}

【问题讨论】:

标签: javascript asynchronous promise api-design


【解决方案1】:

最终是否同步投掷由您决定,而且您很可能会发现任何一方争论不休的人。重要的是记录行为并保持行为的一致性。

我对此事的意见是您的第二个选项 - 将错误传递给回调 - 似乎更优雅。否则,您最终会得到如下所示的代码:

try {
    getUserById(7, function (response) {
       if (response.isSuccess) {
           //Success case
       } else {
           //Failure case
       }
    });
} catch (error) {
    //Other failure case
}

这里的控制流程有点混乱。

在回调中使用单个if / else if / else 结构并放弃周围的try / catch 似乎会更好。

【讨论】:

    【解决方案2】:

    异步函数应该始终以异步方式运行有多重要,尤其是对于错误情况?

    Veryimportant.

    如果您知道程序不处于适合异步操作进行的状态,是否可以throw

    是的,我个人认为这是一个与任何异步产生的错误非常不同的错误,并且无论如何都需要单独处理。

    如果某些用户 ID 因为不是数字而被认为是无效的,而有些用户 ID 将在服务器上被拒绝(例如,因为它们已经被占用),那么您应该始终为这两种情况进行(异步!)回调。如果异步错误仅由网络问题等引起,您可能会发出不同的信号。

    当出现“unexpected”错误时,您总是可以throw。如果您要求有效的用户 ID,您可能会抛出无效的用户 ID。如果您想预测无效的并期望调用者处理它们,您应该使用“统一”错误路由,这将是异步函数的回调/拒绝承诺。

    重复@Timothy:您应该始终记录行为并保持行为的一致性。

    【讨论】:

      【解决方案3】:

      回调 API 理想情况下不应该抛出,但它们确实抛出,因为它很难避免,因为你必须在任何地方都使用 try catch。请记住,throw 明确地抛出错误不是函数抛出所必需的。另一件事是用户回调也可以很容易地抛出,例如调用JSON.parse而不用try catch。

      因此,根据这些理想,这就是代码的样子:

      readFile("file.json", function(err, val) {
          if (err) {
              console.error("unable to read file");
          }
          else {
              try {
                  val = JSON.parse(val);
                  console.log(val.success);
              }
              catch(e) {
                  console.error("invalid json in file");
              }
          }
      });
      

      必须使用 2 种不同的错误处理机制确实很不方便,所以如果你不希望你的程序成为一个脆弱的纸牌屋(永远不要写任何 try catch),你应该使用统一所有异常处理的承诺单一机制:

      readFile("file.json").then(JSON.parse).then(function(val) {
          console.log(val.success);
      })
      .catch(SyntaxError, function(e) {
          console.error("invalid json in file");
      })
      .catch(function(e){
          console.error("unable to read file")
      })
      

      【讨论】:

      • 如果 readFile 抛出异常,它不会被那里定义的 catch 函数捕获。但是如果 readfile 返回一个 promise,那么它就可以工作
      【解决方案4】:

      这在很大程度上是一个见仁见智的问题。无论您做什么,都要始终如一地做,并清楚地记录下来。

      我可以给你的一个客观信息是,这是 JavaScript 的 async 函数设计中的很多讨论的主题,你可能知道它隐含地为他们的工作返回承诺。您可能还知道async 函数在第一个awaitreturn 之前的部分是同步;它只会在awaits 或返回时变为异步。

      TC39 最终决定,即使在 async 函数的同步部分引发的错误也应该拒绝其承诺,而不是引发同步错误。例如:

      async function someAsyncStuff() {
          return 21;
      }
      
      async function example() {
          console.log("synchronous part of function");
          throw new Error("failed");
          const x = await someAsyncStuff();
          return x * 2;
      }
      try {
          console.log("before call");
          example().catch(e => { console.log("asynchronous:", e.message); });
          console.log("after call");
      } catch (e) {
          console.log("synchronous:", e.message);
      }

      您可以看到,即使 throw new Error("failed") 在函数的 同步 部分中,它也会拒绝承诺而不是引发同步错误。

      即使在函数体中第一条语句之前发生的事情也是如此,例如确定缺少的函数参数的默认值:

      async function someAsyncStuff() {
          return 21;
      }
      
      async function example(p = blah()) {
          console.log("synchronous part of function");
          throw new Error("failed");
          const x = await Promise.resolve(42);
          return x;
      }
      try {
          console.log("before call");
          example().catch(e => { console.log("asynchronous:", e.message); });
          console.log("after call");
      } catch (e) {
          console.log("synchronous:", e.message);
      }

      这失败了,因为它试图调用不存在的blah,当它运行代码以获取我在调用中未提供的p 参数的默认值时。如您所见,即使这样也会拒绝承诺,而不是引发同步错误。

      TC39 可能会走另一条路,让同步部分引发同步错误,就像这个非async 函数所做的那样:

      async function someAsyncStuff() {
          return 21;
      }
      
      function example() {
          console.log("synchronous part of function");
          throw new Error("failed");
          return someAsyncStuff().then(x => x * 2);
      }
      try {
          console.log("before call");
          example().catch(e => { console.log("asynchronous:", e.message); });
          console.log("after call");
      } catch (e) {
          console.log("synchronous:", e.message);
      }

      但他们在讨论后决定一致拒绝承诺。

      因此,这是您在决定如何在自己的非async 执行异步工作的函数中处理此问题时要考虑的具体信息。

      【讨论】:

        【解决方案5】:

        理想情况下,您应该拥有一个多层架构,如控制器、服务等。如果您在服务中进行验证,请立即抛出并在您的控制器中设置一个 catch 块来捕获错误格式并发送适当的 http 错误代码。这样您就可以集中所有错误的请求处理逻辑。如果您处理每种情况,您最终会编写更多代码。但这就是我会做的。取决于您的用例

        【讨论】:

          猜你喜欢
          • 2016-11-04
          • 2020-09-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多