JEP 499: Structured Concurrency

This on is HEAVY - leave it for the end of workshops

🎯 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.

Scope Type
Behavior

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 a DomainException), 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