异步方法抛出异常是否合理?

Is it reasonable to throw an exception from an asynchronous method?

在 Java 中开发具有 CompletableFuture return 类型的异步方法,我们希望生成的 CF 正常或异常完成,具体取决于该方法是成功还是失败。

然而,考虑例如我的方法写入 AsynchronousChannel 并在打开该通道时出现异常。它甚至还没有开始写作。所以,在这种情况下,我倾向于让异常流向调用者。对吗?

尽管调用者必须处理 2 种失败情况:1) 异常,或 2) 拒绝承诺。

或者,我的方法是否应该捕获该异常并 return 拒绝承诺?

我认为两者都是有效的设计。 Datastax 实际上从第一种方法开始他们的设计,其中借用连接是阻塞的,然后切换到完全异步模型 (https://docs.datastax.com/en/developer/java-driver/3.5/upgrade_guide/#3-0-4)

作为 datastax java 驱动程序的用户,我对修复非常满意,因为它将 api 更改为真正的非阻塞(甚至打开一个通道,在您的示例中,有成本)。

不过我觉得这里没有对错之分...

从来电者的角度来看,这没有太大区别。在任何一种情况下,异常的原因都是可见的,无论它是从方法中抛出的,还是从可完成的未来调用 get() 中抛出的。

我可能会争辩说,可完成的未来抛出的异常应该是异步计算的异常,而不是未能启动该计算。

IMO,选项 1) 使 API 更难使用,因为将有两种不同的通信错误路径:

  1. "Synchronous" 异常,方法在抛出异常时结束。
  2. "Asynchronous" 异常,其中方法 returns 一个 CF,它以异常完成。请注意,不可能避免这种情况,因为总是会出现只有在异步路径启动后才发现错误的情况(例如超时)。

程序员现在必须确保正确处理这两条路径,而不是只处理一条。

同样有趣的是,C# 和 Javascript 的行为总是通过返回的 Task/[ 报告 async 函数体内抛出的异常。 =14=],即使是在第一个 await 之前抛出的异常,也永远不会以异常结束 async 函数调用。

Kotlin 的协程也是如此,即使使用 Unconfined 调度程序

class SynchronousExceptionExamples {
    @Test
    fun example() {
        log.info("before launch")
        val job = GlobalScope.launch(Dispatchers.Unconfined) {
            log.info("before throw")
            throw Exception("an-error")
        }
        log.info("after launch")
        Thread.sleep(1000)
        assertTrue(job.isCancelled)
    }
}

会产生

6 [main] INFO SynchronousExceptionExamples - before launch
73 [main @coroutine#1] INFO SynchronousExceptionExamples - before throw
(...)
90 [main] INFO SynchronousExceptionExamples - after launch

请注意,异常发生在 main 线程中,但是 launch 以正确的 Job 结尾。