ursus
08/19/2026, 12:58 AMChannel behaves in the limit
val events = Channel<Event>()
scope.launch {
events.send(SomeEvent(42))
}
differentScope.launch {
for (event in events) {
doSomethingBlocking(event)
}
}
imagine this setup
so a randezevous channel, the semantics are events.send(SomeEvent(42)) won't resume until it's delivered
Is that really the case?
From what I see that loop is sugar for
while (iterator.hasNext()) {
val element = iterator.next()
// ... handle the element ...
}
So two suspension points..
and I'm not super clear on the docs of hasNext (kotlinlang.org/api/kotlinx.coroutines/…/has-next.html)
It seems hasNext is the one which dequeues the buffer, so .. is that what signals the events.send to resume? or is it the next? There is this note of "p*rompt cancellation guarantee"* but I can't really parse it.
In other words, can there be a situation where events.send resumes but the other side not even start doSomethingBlocking due to cancellation?Youssef Shoaib [MOD]
08/19/2026, 1:06 AMIf the channel has an element available, returnsand removes it from the channel.true
next is not even suspend, so only hasNext can possibly be taking the element out.ursus
08/19/2026, 1:08 AMursus
08/19/2026, 1:09 AMYoussef Shoaib [MOD]
08/19/2026, 1:10 AMBecause of the prompt cancellation guarantee, some values retrieved from the channel can become lost. See the "Undelivered elements" section in the Channel documentation for details on handling undelivered elements.The channel docs say:
Thefactory function has the optional parameterChannel(). When that parameter is set, the corresponding function is called once for each element that was sent to the channel with the call to the send function but failed to be delivered, which can happen in the following cases:onUndeliveredElement
[...]
• When the receiving operations like receive, onReceive, or hasNext throw an exception after retrieving the element from the channel because of being cancelled before the code following them had a chance to resume.so it seems like a yes!
ursus
08/19/2026, 1:12 AMChannels
pretending it was delivered when it wasn't...not a great default
now I have to .. idk, pass mutablestateflows and let receiver clear the pending state masquarading as event if I want a delivery guarantee?Youssef Shoaib [MOD]
08/19/2026, 1:24 AMhasNext(), receive, etc actually has a coroutineContext.ensureActive() right after it. That should fix the mental model hopefully
If you really want, you can always force it to not happen:
withContext(NonCancellable) { channel.receive() }
You could pretty easily do the same with channel iterators (untested code):
fun <T> Channel<T>.nonCancellable(): ChannelIterator<T> {
val iterator = iterator()
object: ChannelIterator<T> by iterator {
override suspend fun hasNext() = withContext(NonCancellable) { iterator.hasNext() }
}
}
// you might need this as well, not sure
operator fun <T> ChannelIterator<T>.iterator(): ChannelIterator<T> = thisursus
08/19/2026, 1:30 AMNotCancellable is the way to go, what would make the loop uncancellable at all times right?ursus
08/19/2026, 1:36 AMAll suspending functions with channels provide prompt cancellation guarantee. If the job was cancelled while send or receive function was suspended, it will not resume successfully, even if it already changed the channel's state, but throws a CancellationException. With a single-threaded dispatcher like Dispatchers.Main, this gives a guarantee that the coroutine promptly reacts to the cancellation of its Job and does not resume its execution.I'm really trying to understand this but somehow escapes me
it will not resume successfully, even if it already changed the channel's statewhat is here meant to resume? the other side? or the same side (which got cancelled)
Youssef Shoaib [MOD]
08/19/2026, 2:01 AMhasNext() is actually secretHasNext().also { coroutineContext.ensureActive() }
The iterator I've provided keeps the loop cancellable I'm pretty sure. It only wraps the hasNext in NonCancellable.ursus
08/19/2026, 2:08 AMDmitry Khalanskiy [JB]
08/19/2026, 7:29 AMhasNext suspends awaiting an element.
• Someone `send`s an element to the channel.
• hasNext grabs that element, and its code is getting scheduled to execute on its CoroutineDispatcher.
• While hasNext is in the dispatcher's queue, the coroutine running hasNext gets cancelled.
• (Here's where the prompt cancellation guarantee comes into play) Instead of running, hasNext finishes with a CancellationException. The element that hasNext grabbed has already left the channel, it's not going to be put back in, so now it's simply dropped (and onUndeliveredElement gets invoked).
Without prompt cancellation guarantee, hasNext would resume, even though its coroutine already got cancelled.
The secretHasNext().also { coroutineContext.ensureActive() } model is good!ursus
08/19/2026, 11:52 AMsend resume? whats the trigger?
a) hasNext "grabbing" that element? Or
b) hasNext returning successfully?
c) something else?Dmitry Khalanskiy [JB]
08/19/2026, 11:52 AMursus
08/19/2026, 11:57 AMb behavior somehow?
I dont mind value staying in the queue and getting processed later when conditions are met again, just dequeueing and not even starting processing..idk how to reason over such system, because this is not a function of backpressure or channel confkg or anything, all it seems I can do is to log in onUndeliveredElement and ..shrug
Is then there a better primitive?ursus
08/19/2026, 11:59 AMDmitry Khalanskiy [JB]
08/19/2026, 12:00 PMmaybe onUndelivered send it again?Nope, won't work,
onUndeliveredElement isn't a suspend callback but a normal function.ursus
08/19/2026, 12:04 PMDmitry Khalanskiy [JB]
08/19/2026, 12:10 PMursus
08/19/2026, 12:17 PMursus
08/19/2026, 12:19 PMursus
08/19/2026, 1:46 PMursus
08/19/2026, 4:57 PMChannel, StateFlow, SharedFlow, is there something else?Dmitry Khalanskiy [JB]
08/20/2026, 10:50 AMStateFlow representing a queue of commands to execute on the UI looks like a good fit for your scenario, but maybe you could benefit from using Jetpack Compose, which is also built on that idea.
Other than that, it does look like your use case calls exactly for an atomic `Channel`: you send values where each value needs to be processed by exactly one consumer. Please take a look at github.com/Kotlin/kotlinx.coroutines/issues/4414#… for a way to disable prompt cancellation selectively. Here, you'd need to use onReceiveCatching as the select clause.ursus
08/20/2026, 2:54 PMclass AckedEventQueue<T> {
fun add(event: T)
suspend fun next(): Element<T>
fun ack(id: Long)
data class Element<T>(val id: Long, val event: T)
}
what would you say to a purpose built thing, something like this? So it explicitly dequeues not in next but after `ack`ing it. But it does mix suspend/blocking semantics so idk, what do you think? Or would the atomic channel extensions be more idiomatic?
(Also the deque backing this technically is also state, so google should be happy also 😄 )Dmitry Khalanskiy [JB]
08/24/2026, 9:10 AMChannel, sure, makes sense! I personally would go for an extension like atomicReceive().