稍后赶上承诺拒绝[重复]

Posted

技术标签:

【中文标题】稍后赶上承诺拒绝[重复]【英文标题】:Catch Promise rejection at a later time [duplicate] 【发布时间】:2019-05-21 00:42:07 【问题描述】:

我如何在以后检索承诺的结果?在测试中,我在发送更多请求之前检索了一封电子邮件:

const email = await get_email();
assert.equal(email.subject, 'foobar');
await send_request1();
await send_request2();

在缓慢的电子邮件检索过程中如何发送请求?

起初,我考虑过等一下邮件:

// This code is wrong - do not copy!
const email_promise = get_email();
await send_request1();
await send_request2();
const email = await email_promise;
assert.equal(email.subject, 'foobar');

如果get_email() 成功,则此方法有效,但如果get_email() 在相应的await 之前失败,则此方法失败,并带有完全合理的UnhandledPromiseRejectionWarning

当然,我可以使用Promise.all,像这样:

await Promise.all([
    async () => 
        const email = await get_email();
        assert.equal(email.subject, 'foobar');
    ,
    async () => 
        await send_request1();
        await send_request2();
    ,
]);

但是,它使代码更难阅读(它看起来更像基于回调的编程),特别是如果以后的请求实际上依赖于电子邮件,或者存在一些嵌套。是否可以在以后存储承诺的结果/异常和await

如果需要,here is a testcase 带有有时会失败但有时会工作的模拟,时间随机。它绝不能输出UnhandledPromiseRejectionWarning

【问题讨论】:

我不明白这个。使用 async/await 时,您可以使用 try/catch 捕获错误。您可以使用 .catch() 链接到承诺对象。您发布的缓冲区解决方案基本上只是 .then() 的实现。因此,只要您的承诺被拒绝,稍后添加 .catch() 就会给您错误。 @Shilly 太好了,介意发布您的解决方案吗?我在下面的答案包含一个完整的测试(有时会失败,有时会工作,时间不同)。在节点中多次运行时,此缓冲区答案总是输出正确的结果,并且 never 会导致 UnhandledPromiseRejectionWarning。如果我只需要一对awaits 或thens,那就更好了! 什么意思?你需要捕捉个别错误吗?是不是你想要一个 UnhandledPromiseRejectionWarning 而不是抓住它? 不,我确实想要任何UnhandledPromiseRejectionWarnings。任何错误都应该像往常一样在 Promise 中抛出。换句话说,示例代码应该始终工作或捕获异常(输出main error: Error: failure)。 真的有必要用同样的方法检索邮件和发送请求吗?如果可能的话,分派三个单独的线程似乎会更干净。您在这里有效地尝试使用不适合这项工作的工具来解决并发问题。 【参考方案1】:
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const send_request1 = () => wait(300), send_request2 = () => wait(200);
async function get_email() 
    await wait(Math.random() * 1000);
    if (Math.random() > 0.5) throw new Error('failure');
    return subject: 'foobar';


const assert = require('assert');
async function main() 
    // catch possible error
    const email_promise = get_email().catch(e => e);
    await send_request1();
    await send_request2();
    // wait for result
    const email = await email_promise;
    // rethrow eventual error or do whatever you want with it
    if(email instanceof Error) 
      throw email;
    
    assert.equal(email.subject, 'foobar');
;

(async () => 
    try 
        await main();
     catch(e) 
        console.log('main error: ' + e.stack);
    
)();

【讨论】:

只需将顶行更改为const email_promise = get_email(); email_promise.catch(e => e),不再需要所有重新抛出的代码,是吗? 重新抛出代码是必要的,没有它会'resolve'并出现错误,例如'resolve(new Error ())'【参考方案2】:

如果保证以后会处理 Promise 拒绝,则可以将 Promise 与 dummy catch 链接以抑制检测未处理的拒绝:

try 
    const email_promise = get_email();
    email_promise.catch(() => ); // a hack
    await send_request1();
    await send_request2();
    const email = await email_promise;
    assert.equal(email.subject, 'foobar');
 catch (err) ...

这种方法的问题是有两个并发例程,但代码没有表达这一点,这是通常使用Promise.all 完成的解决方法。这种解决方法可行的唯一原因是只有 2 个例程,其中一个 (get_email) 只需要与 then/await 链接一次,所以它的一部分 (assert)可以延期。如果有 3 个或更多例程,或者例程涉及多个then/await,问题会更加明显。

如果 Promise.all 引入了不需要的 lambda 嵌套级别,可以通过将例程编写为命名函数来避免这种情况,即使它们没有在其他任何地方重用:

async function assertEmail() 
    const email = await get_email();
    assert.equal(email.subject, 'foobar');


async function sendRequests() 
    await send_request1();
    await send_request2();


...

try 
    await Promise.all([assertEmail(), sendRequests()]);
 catch (err) ...

这会产生清晰的控制流和冗长但更易于理解和可测试的代码。

【讨论】:

你能详细说明 3 个或更多 Promise 的问题吗?因为那是我的代码中实际拥有的。我的真实代码中的问题是以后的请求可能取决于电子邮件。此外,它作为一系列 await 语句更具可读性,特别是如果它只允许我们原生表达同步点(= 所有 3 个并发任务必须在此处完成),没有嵌套或辅助函数. 它作为一系列等待语句更具可读性 - 这里让我感到困惑的是你任意选择了一个例程(一系列 send_request*)来编写它以类似同步的方式,而另一个例程(get_email)横向运行。如果有更多的承诺,控制流程将更难遵循。为什么这些例程中的一个应该被认为是一流的并且是连续编写的,而其他例程却是横向的?这在这里看起来很可疑,因为通常这样的例程被视为平等。 get_email 是一个缓慢的操作,不需要进一步的监督,所以是的,它对主要流程并不重要,至少在我真正需要它的返回值之前。这对于并发应用程序来说真的那么不寻常吗?【参考方案3】:

所以,我想解释一下为什么我们在 Node.js 中会这样:

// Your "incorrect code" from before
const email_promise = get_email(); // we acquire the promise here
await send_request1(); // if this throws - we're left with a mess
await send_request2(); // if this throws - we're left with a mess
const email = await email_promise;
assert.equal(email.subject, 'foobar');

也就是说,我们这样做的原因是不处理“多次拒绝并且可能没有清理”的情况。我不确定你是如何得到Promise.all 的长代码的,但是这个:

await Promise.all([
    async () => 
        const email = await get_email();
        assert.equal(email.subject, 'foobar');
    ,
    async () => 
        await send_request1();
        await send_request2();
    ,
]);

其实可以这样:

let [email, requestData] = await Promise.all([
  get_email(),
  send_request1().then(send_request2)
]);
// do whatever with email here

这可能是我会做的。

【讨论】:

嗯,这就是为 *** 简化代码的缺点:我的真实代码要复杂得多(这是一个集成测试)。发送了十几个请求,使用同步代码来计算值——这只是现实生活中的代码。用一个简单的async..await 表达一些请求的异步性将是awesome,并且具有更多的可读性然后有一堆嵌套的Promise.all

以上是关于稍后赶上承诺拒绝[重复]的主要内容,如果未能解决你的问题,请参考以下文章

使用 fetch 时拒绝承诺 [重复]

等待但从未解决/拒绝承诺内存使用[重复]

重复执行时未处理的承诺拒绝

mongoose 的重复键错误处理和未处理的承诺拒绝

稍后解决承诺

在 then 块中拒绝返回的承诺