【问题标题】:Should I avoid try catch in every single async/await on Node js?我应该避免在 Node js 上的每一个异步/等待中尝试捕获吗?
【发布时间】:2020-08-04 16:55:08
【问题描述】:

这是我在单元测试时遇到的一个设计问题。 让我们深入示例:

想象一下:

async function foo() {
    try {
        return apiCall()
    }
    catch (e) {
        throw new CustomError(e);
    } 
}



async function bar() {
    return foo()
}



async function main() {
    try {
        await bar()
    }catch(e) {
        console.error(e)
    }
}

main()

我们在这里看到了什么?唯一没有 try-catch 块的函数是 bar。 但是如果 foo 失败了,它应该被 main catch 捕获。

像这样进行单元测试时

describe('testing bar', () => {
    it('foo should throw', () => {
        foo.mockImplementantion(() => { throw new CustomError('error')});
        bar()
        .then((result) => console.log(result))
        .catch((err) => { exepect(err).toBeInstanceOf(CustomError)}) // this is what we are testing
    })
})

我们看到的输出是控制台中记录了一个未处理的承诺拒绝

所以,我的问题是……即使我知道 main() 会捕获错误,我是否应该在所有异步函数中使用 try-catch 块?

【问题讨论】:

  • 因为 node.js 进程可以/将因未处理的 Promise 拒绝而终止,建议您始终使用 try/catch 和 await,并始终将 .catch 处理程序添加到 Promise 链的末尾.
  • 你告诉我的是我应该将函数 bar() 包装在一个 try catch 块中,对吧?
  • 异步函数 bar 返回一个承诺,对吗?异步函数 foo 可以在它的 catch 块中抛出一个异常,这会导致 bar 中的 未处理的承诺拒绝,所以........
  • 您的测试用例没有return 承诺或使用async/await 也没有done 回调,因此expect 可能会抛出关于在测试结束后被调用。未处理的拒绝的错误信息是什么?
  • @Bergi (node:44872) UnhandledPromiseRejectionWarning: CustomError

标签: javascript node.js async-await jestjs


【解决方案1】:

如果函数能够从错误中恢复、执行日志记录等副作用或重新引发更有意义的错误,则可能需要try..catch

如果CustomErrorapiCall 可以抛出的错误更可取,那么try..catch 是必要的,否则不会。 foo 的问题还在于它只处理同步错误。为了处理被拒绝的promise,它应该是return await apiCall(),这是async的一个已知陷阱。

未捕获的拒绝是不受欢迎的,它们目前会导致 UnhandledPromiseRejectionWarning 并且预计会在未来的版本中使 Node 崩溃。最好在顶层以有意义的方式处理错误,因此main 需要捕获错误。这可以委托给 process uncaughtRejection 事件处理程序,但它可能有益于它保持不应达到的额外错误处理级别。

我们看到的输出是控制台中记录了 Unhandled Promise 拒绝。

这不应该发生。拒绝需要由测试处理。上面解释了一个可能的故障点,foo 可以从apiCall 而不是CustomError 返回原始错误,以防它没有正确模拟,这将导致预期失败并导致catch() 中未处理的拒绝。另一个失败点是测试有 unchained promise,因为它没有被返回,测试总是通过。

使用 Promise 的异步测试应该总是返回一个 Promise。这可以通过使用async..await 来改进。 fooasync,它应该总是返回一个承诺:

it('foo should throw', async () => {
    foo.mockImplementantion(() => { return Promise.reject(new CustomError('error')) });
    await expect(bar()).rejects.toThrow(CustomError);
})

现在,即使 foo 模拟失败(foo 模拟不会影响 bar,如果它们在所示的相同模块中定义)并且 bar 拒绝了不是 CustomError 的东西,这将被断言。

【讨论】:

  • 我有关于这个 1 的三个 cmets。如果函数能够从错误中恢复、执行日志记录等副作用或重新抛出更有意义的错误,则可能需要 try..catch。没错,在这种情况下 2 没关系。我很确定你不能 return await.... 你应该只使用 return 3。我在测试中更改了一些代码,现在正在通过
  • @JulianMendez return await 当然是可能的,even necessary inside try blocks
  • @JulianMendez 那么毕竟只是测试用例被破坏了?你能告诉我们你是怎么解决的吗?
【解决方案2】:

没有。您不需要需要在每个 async/await 中使用 try/catch。您只需需要在顶层执行此操作。在这种情况下,您已经在执行 main 函数。

应该的天气是一个见仁见智的问题。 go 语言设计者对此有足够的感觉,这已成为go 中的标准,始终在每个函数调用时处理错误。但这不是 javascript 或大多数其他语言的规范。

未处理的承诺拒绝

it() 函数抛出未处理的 Promise 拒绝,因为您没有告诉它等待 Promise 完成。

我假设您使用 mocha 之类的东西进行单元测试(其他框架的工作方式可能不同)。在 mocha 中有两种处理异步测试的方法:

  1. 调用 done 回调 - it() 函数将始终使用 done 回调来调用。取决于你想使用它的天气,或者在你发布的代码中不使用它:

     describe('testing bar', () => {
         it('foo should throw', (done) => {
             foo.mockImplementantion(() => { throw new CustomError('error')});
             bar()
             .then((result) => {
                 console.log(result);
                 done(); // ------------- THIS IS YOUR ACTUAL BUG
              })
             .catch((err) => {
                 exepect(err).toBeInstanceOf(CustomError);
                 done(); // ------------- THIS IS YOUR ACTUAL BUG
             })
         })
     })
    
  2. 返回一个承诺。如果您向it() 函数返回一个promise,mocha 将意识到您的代码是异步的并等待完成:

     describe('testing bar', () => {
         it('foo should throw', (done) => {
             foo.mockImplementantion(() => { throw new CustomError('error')});
    
             return bar() // <----------- THIS WOULD ALSO FIX IT
             .then((result) => {
                 console.log(result);
              })
             .catch((err) => {
                 exepect(err).toBeInstanceOf(CustomError);
             })
         })
     })
    

简而言之,您的代码没有任何问题。但是您的单元测试中有一个错误

【讨论】:

  • 请注意,此答案只是改写@Bergi 试图在 cmets 中告诉您的内容。你对他的评论的反应告诉我你不明白他想告诉你什么(不,你不需要修改你的代码 - 修改你的单元测试)所以我详细说明了。
  • 哦,对了,我实际上理解了@Bergi 对我说的一半。我的问题有很多解决方案,我认为和你一样:我不需要在我使用的每个 async/await 中使用 try/catch 块。我将发布另一个解决问题的方法
  • 这是 Jest,但在这方面它的工作方式与 Mocha 相同。 1 可以跳过,因为它显示了承诺中的done 有什么问题。大多数时候,开发人员没有足够的纪律来保证正确的控制流程。在这种情况下,done 在断言之后将导致测试超时,因为断言失败时它永远不会被调用。它应该是 .then(() =&gt; done(), done.fail) 作为覆盖所有基础的最后一个链......除了它不应该是因为当返回一个承诺时这已经完成,如 2 所示。TL;DR:不要使用 done承诺。
【解决方案3】:

正如@Bergi 告诉我的,我会在这里发布一些解决方案

我将函数包装在 try catch 块中

1.

async function bar() {
    try{
       return foo()
    } catch (e) {
       throw e
    }
}
  1. 重写测试
describe('testing bar', () => {
    it('foo should throw', (done) => {
        foo.mockImplementantion(() => { throw new CustomError('error')});
        bar()
        .then((result) => { throw result }) // this is because we are expecting an error, so if the promise resolves it's actually a bad sign.
        .catch((err) => { 
          exepect(err).toBeInstanceOf(CustomError)}) // this is what we are testing
          done();
    })
})
  1. 在测试用例中使用return
describe('testing bar', () => {
    it('foo should throw', () => {
        foo.mockImplementantion(() => { throw new CustomError('error')});
        return bar()
        .then((result) => { throw result })
        .catch((err) => { exepect(err).toBeInstanceOf(CustomError)}) // this is what we are testing
    })
})

【讨论】:

  • 1. try..catch in try{ return foo() } catch (e) { throw e } 是无操作的,关于这个问题,是的,与原始的 foo 相比,这是你当然想要避免的事情。由于async 的工作方式,它仍然无法捕获异步错误。 2. 当 done 不可达时会导致测试超时,这就是为什么 done 不应该与 promises 一起使用的原因,它们在测试中提供了优越的控制流。 3..then((result) =&gt; { throw result })会导致误报,以防foo错误地返回CustomError而不是抛出,刚刚看到一个问题实际上是这样做的。
  • bar() 返回CustomError 时再次误报:您应该使用then(result =&gt; { throw new Error("Expected rejection, got "+result); }, err =&gt; { exepect(err).toBeInstanceOf(CustomError)}) 而不是处理抛出结果的.catch()
猜你喜欢
  • 2017-05-17
  • 1970-01-01
  • 2017-12-23
  • 2014-12-01
  • 2019-10-20
  • 2019-06-30
  • 1970-01-01
  • 2018-04-20
  • 2020-08-06
相关资源
最近更新 更多