异步 API 应该同步抛出吗?

IT技术 javascript asynchronous promise api-design
2021-01-24 03:45:31

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

如果我立即知道为该函数提供的参数无效,该函数应该throw同步,还是应该返回被拒绝的Promise(或者,如果您愿意,可以使用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
}
5个回答

最终是否同步抛出的决定取决于你,你可能会发现有人争论任何一方。重要的是记录行为并保持行为的一致性。

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

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

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

似乎if / else if / else在回调中有一个单一的结构并放弃周围的try / catch.

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

重要

是否确定throw,如果你知道程序是不是一个合适的状态的异步操作继续进行?

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

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

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

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

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

我可以给你的一条客观信息是,这是 JavaScriptasync函数设计中经常讨论的主题,你可能知道这些函数隐式地返回了对它们的工作的Promise。您可能还知道第一个async函数之前的部分await或者return同步的它仅在它awaits 或返回时才变为异步

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

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")同步部分,它也拒绝Promise而不是引发同步错误。

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

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

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

但他们经过讨论后决定采用一致的Promise拒绝。

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

回调 API 理想情况下不应该抛出,但它们确实抛出,因为很难避免,因为您必须在任何地方都尝试 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),你应该使用 Promise 将所有异常处理统一在一个机制下:

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,那么它就可以工作
2021-03-16 03:45:31

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