JEP 499: Structured Concurrency
This on is HEAVY - leave it for the end of workshops
https://250bpm.com/ = whole "structured concurrency" section
https://kotlinlang.org/docs/cancellation-and-timeouts.html#cancellation-is-cooperative - and why currently it is not the problem - you can not cancell tasks/coroutines
🎯 Purpose of Structured Concurrency (JEP 499)
✅ Makes concurrent code easier to reason about
Tasks are scoped, just like variables in a block — when the scope ends, all tasks are done.
🔒 Ensures safe lifecycle management of threads
No more forgotten threads running in the background or leaking resources.
🔁 Joins and handles subtasks consistently
You explicitly wait for all subtasks to complete and handle their outcomes in one place.
📦 Propagates errors and cancellation properly
A failure in one task cancels the others and surfaces at the top level.
🧵 Plays perfectly with virtual threads
Structured scopes simplify and manage thousands of lightweight virtual threads safely.
🧼 Encourages clean and reliable resource usage
Threads are tied to lexical scopes, similar to try-with-resources or auto-closing blocks.
🛑 Makes cancellation cooperative and safe
You can shut down a scope and be confident that all work will stop quickly and gracefully.
🧠 Improves debuggability and predictability
Scopes encapsulate related tasks, making stack traces and failure analysis much clearer.
ShutdownOnSuccess
Stops when any subtask succeeds. Cancels others.
ShutdownOnFailure
Stops when any subtask fails. Cancels others.
StructuredTaskScope
Manual shutdown control — you must call shutdown()
explicitly.
Demo
Start with problem
var result1=async(...)
var result2=async(...)
return result1+result2;
com.wlodar.jeeps.jep449structuredConcurrency.SimpleStructuredConcurrencyDemo
It creates Task scope with two subtasks where each runs in its own virtual thread. Scope is created with custom thread factory so that virtual threads have names :
Run :
ScopeSharedAcrossThreadsDemo
It presents that only "owner" can operate on scope. You can not call methods on scope from independet thread
Run :
StructuredConcurrencyCancellationDemo
It shows that failure of one task fails whole operation tree. It is important to see here that receiver thread needs to check if there is interruption signal sent. Also check what will happen when you catch Interrupted exception
Run :
NestedStructuredShutdownDemo
It shows how to create sub scopes. Also shows that after shutdown subtasks failures are ignored.
How they avoided Kotlin Trap
🔥 TL;DR
If you cancel a coroutine (i.e., a Job), and that coroutine catches the
CancellationException
and throws a different exception (like aDomainException
), the new exception will not be discarded — it will be propagated, and can cancel the parent and its siblings.
⚠️ Why this is dangerous?
Because Kotlin uses CancellationException
as a signal to cancel a coroutine, swallowing or replacing it breaks the structured concurrency flow.
If you do this:
kotlinCopyEdittry {
delay(1000)
} catch (e: Exception) {
// This eats CancellationException and rethrows something else
throw DomainException("Oops!", e)
}
Then even though this coroutine was cancelled intentionally, you're reporting it as a failure.
So what happens?
✅ Yes, the DomainException
is handled (not ignored)...
😱 But, the coroutine framework will treat this as a failure, and cancel the parent and all siblings.
✅ Best Practice
kotlinCopyEdittry {
...
} catch (e: CancellationException) {
throw e // ✅ propagate cancellation properly
} catch (e: Exception) {
throw DomainException("Business failure", e)
}
Last updated