Skip to content

Commit 8eb4963

Browse files
committed
Improve the explanation of how await throws exceptions
Fixes two issues: * It is surprising for some users that the same exception can be thrown several times. Clarified this point explicitly. * Due to #3658, `await` can throw `CancellationException` in several cases: when the `await` call is cancelled, or when the `Deferred` is cancelled. This is clarified with an example of how to handle this. Fixes #3937
1 parent fdc0818 commit 8eb4963

File tree

2 files changed

+51
-11
lines changed

2 files changed

+51
-11
lines changed

kotlinx-coroutines-core/common/src/Deferred.kt

+32-10
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,46 @@ import kotlinx.coroutines.selects.*
3333
public interface Deferred<out T> : Job {
3434

3535
/**
36-
* Awaits for completion of this value without blocking a thread and resumes when deferred computation is complete,
37-
* returning the resulting value or throwing the corresponding exception if the deferred was cancelled.
36+
* Awaits for completion of this value without blocking the thread and returns the resulting value or throws
37+
* the exception if the deferred was cancelled.
38+
*
39+
* Unless the calling coroutine is cancelled, [await] will return the same result on each invocation:
40+
* if the [Deferred] completed successfully, [await] will return the same value every time;
41+
* if the [Deferred] completed exceptionally, [await] will rethrow the same exception.
42+
*
43+
* This suspending function is itself cancellable: if the [Job] of the current coroutine is cancelled or completed
44+
* while this suspending function is waiting, this function immediately resumes with [CancellationException].
45+
*
46+
* This means that [await] can throw [CancellationException] in two cases:
47+
* - if the coroutine in which [await] was called got cancelled,
48+
* - or if the [Deferred] itself got completed with a [CancellationException].
49+
*
50+
* In both cases, the [CancellationException] will cancel the coroutine calling [await], unless it's caught.
51+
* The following idiom may be helpful to avoid this:
52+
* ```
53+
* try {
54+
* deferred.await()
55+
* } catch (e: CancellationException) {
56+
* currentCoroutineContext().ensureActive() // throws if the current coroutine was cancelled
57+
* processException(e) // if this line executes, the exception is the result of `await` itself
58+
* }
59+
* ```
3860
*
39-
* This suspending function is cancellable.
40-
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
41-
* immediately resumes with [CancellationException].
4261
* There is a **prompt cancellation guarantee**. If the job was cancelled while this function was
4362
* suspended, it will not resume successfully. See [suspendCancellableCoroutine] documentation for low-level details.
4463
*
45-
* This function can be used in [select] invocation with [onAwait] clause.
46-
* Use [isCompleted] to check for completion of this deferred value without waiting.
64+
* This function can be used in [select] invocations with an [onAwait] clause.
65+
* Use [isCompleted] to check for completion of this deferred value without waiting, and
66+
* [join] to wait for completion without returning the result.
4767
*/
4868
public suspend fun await(): T
4969

5070
/**
51-
* Clause for [select] expression of [await] suspending function that selects with the deferred value when it is
52-
* resolved. The [select] invocation fails if the deferred value completes exceptionally (either fails or
53-
* it cancelled).
71+
* Clause using the [await] suspending function as a [select] clause.
72+
* It selects with the deferred value when the [Deferred] completes.
73+
* If [Deferred] completes with an exception, the whole the [select] invocation fails with the same exception.
74+
* Note that, if [Deferred] completed with a [CancellationException], throwing it may have unintended
75+
* consequences. See [await] for details.
5476
*/
5577
public val onAwait: SelectClause1<T>
5678

kotlinx-coroutines-core/common/test/selects/SelectDeferredTest.kt

+19-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package kotlinx.coroutines.selects
55
import kotlinx.coroutines.testing.*
66
import kotlinx.coroutines.*
77
import kotlin.test.*
8+
import kotlin.time.Duration.Companion.seconds
89

910
class SelectDeferredTest : TestBase() {
1011
@Test
@@ -114,6 +115,23 @@ class SelectDeferredTest : TestBase() {
114115
finish(9)
115116
}
116117

118+
/**
119+
* Tests that completing a [Deferred] with an exception will cause the [select] that uses [Deferred.onAwait]
120+
* to throw the same exception.
121+
*/
122+
@Test
123+
fun testSelectFailure() = runTest {
124+
val d = CompletableDeferred<Nothing>()
125+
d.completeExceptionally(TestException())
126+
val d2 = CompletableDeferred(42)
127+
assertFailsWith<TestException> {
128+
select {
129+
d.onAwait { expectUnreached() }
130+
d2.onAwait { 4 }
131+
}
132+
}
133+
}
134+
117135
@Test
118136
fun testSelectCancel() = runTest(
119137
expected = { it is CancellationException }
@@ -167,4 +185,4 @@ class SelectDeferredTest : TestBase() {
167185
override val list: NodeList?
168186
get() = error("")
169187
}
170-
}
188+
}

0 commit comments

Comments
 (0)