Help us improve
Share bugs, ideas, or general feedback.
Expert guidance on Kotlin Coroutines: structured concurrency, scopes, dispatchers, cancellation, exception handling, Flow, Channels, testing (virtual time), and lifecycle-aware collection on Android. Use when writing or reviewing async Kotlin code.
npx claudepluginhub santimattius/structured-coroutines --plugin kotlin-coroutines-skillHow this skill is triggered — by the user, by Claude, or both
Slash command
/kotlin-coroutines-skill:kotlin-coroutines-skillThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill provides expert guidance on Kotlin Coroutines, covering structured concurrency, scopes,
ref-examples.ymlref-manifest.ymlreferences/ref-1-1-global-scope.mdreferences/ref-1-2-async-without-await.mdreferences/ref-1-3-breaking-structured-concurrency.mdreferences/ref-1-4-awaitall-exception-propagation.mdreferences/ref-101-interop-suspend-cancellable.mdreferences/ref-102-interop-callbackflow-awaitclose.mdreferences/ref-103-interop-channelflow-callbackflow.mdreferences/ref-104-interop-future-get.mdreferences/ref-111-kmp-dispatchers-io.mdreferences/ref-112-kmp-runblocking.mdreferences/ref-113-kmp-mainscope-without-cancel.mdreferences/ref-121-concur-synchronized.mdreferences/ref-122-concur-shared-mutable-state.mdreferences/ref-131-backend-blocking.mdreferences/ref-141-debug-coroutine-name.mdreferences/ref-15-concur-sequential-async.mdreferences/ref-2-1-launch-last-line-coroutine-scope.mdreferences/ref-2-2-runblocking-in-suspend.mdGuides Kotlin coroutine usage for structured concurrency: suspend functions, builders, Flow, channels, async patterns, cancellation, and exception handling.
Reviews Kotlin coroutine code for structured-concurrency violations: stored CoroutineScope, init-block launches, non-suspending APIs calling launch, runBlocking misuse, and swallowed CancellationException.
Provides patterns for Kotlin Coroutines and Flows: structured concurrency, StateFlow, combining flows, parallel decomposition, and testing in Android and KMP projects.
Share bugs, ideas, or general feedback.
This skill provides expert guidance on Kotlin Coroutines, covering structured concurrency, scopes,
Dispatchers (including main-safe suspend and dispatcher injection), cancellation (including
withTimeout semantics), exception handling (CoroutineExceptionHandler, launch vs async),
Channels, Flow (cold vs hot, collectLatest, SharedFlow configuration, blocking in flow {}),
lifecycle-aware collection on Android, and testing (virtual time, setMain/resetMain). Use this
skill to help developers write safe, maintainable concurrent code aligned with Kotlin 1.9+/2.0+
conventions and official best practices.
references/.GlobalScope in production. Use framework scopes (viewModelScope,
lifecycleScope, rememberCoroutineScope), injected scopes, or local scopes (
coroutineScope { }, withContext { }). If an external scope is required, justify and document
it.async only when a return value is needed; if await() is never called, use launch.
Preserve structured concurrency: inside suspend functions use coroutineScope { } + async/
launch; do not launch in an external scope from suspend unless work must outlive the flow, and
then document it.runBlocking inside suspend functions or coroutine-based code. Avoid ending a suspend
function with coroutineScope { launch { } } as the last line when the intent is fire-and-forget
— coroutineScope waits for all children and blocks the caller; use an explicit external scope
and document it if the work must truly run in the background beyond the caller's lifetime.Dispatchers.Default for CPU-bound work, Dispatchers.Main/
Main.immediate for UI, withContext(Dispatchers.IO) for blocking I/O. Never perform blocking
I/O on Default or Main. Do not use Dispatchers.Unconfined in production unless for a rare,
documented case. Make suspend functions main-safe: move blocking work into
withContext(Dispatchers.IO) so callers on Main are never blocked. Inject CoroutineDispatcher
as a constructor parameter (default to real dispatcher; replace with TestDispatcher in tests).Job() or SupervisorJob() directly to builders (e.g. launch(Job()) { }). Use
supervisorScope { } or a scope defined with SupervisorJob() for supervisor semantics. When
running independent tasks with awaitAll, use supervisorScope instead of coroutineScope so
one failure does not cancel sibling deferreds.CancellationException; rethrow it in catch blocks.CancellationException for domain errors; use normal exceptions instead.yield(), ensureActive(), or while (isActive)
with delay(interval) so the coroutine responds to cancellation.finally, use withContext(NonCancellable) { }.scope.cancel(); use coroutineContext.job.cancelChildren() to
stop only children while keeping the scope alive.withTimeoutOrNull over withTimeout to avoid unintentionally cancelling the parent
scope. If using withTimeout, catch TimeoutCancellationException explicitly. Always ensure
resources opened inside withTimeout are cleaned up in finally.launch propagate to CoroutineExceptionHandler; in async, the
exception is stored in the Deferred and only thrown on await(). Always call await() on
async blocks to avoid silently losing exceptions.CoroutineExceptionHandler at scope level for launch uncaught exceptions.kotlinx-coroutines-test: runTest, virtual time, advanceTimeBy,
advanceUntilIdle, and inject TestDispatcher/StandardTestDispatcher; avoid real delay()
with runBlocking. Replace Dispatchers.Main using Dispatchers.setMain(TestDispatcher()) in
@Before and Dispatchers.resetMain() in @After.produce { } for channels so they close when the coroutine ends. Do not share
consumeEach across multiple consumers; use for (x in channel) per consumer.flow { } builder non-blocking; use flowOn(Dispatchers.IO) or suspend APIs.StateFlow for shared UI state (replays last value); use SharedFlow for events with
explicit replay, extraBufferCapacity, and onBufferOverflow configuration.collectLatest only when cancelling in-progress work is intentional (e.g. search); use
collect when each item must be processed to completion.repeatOnLifecycle(Lifecycle.State.STARTED) or
flowWithLifecycle to stop collection when the UI goes to background.When analyzing Kotlin projects for coroutine issues:
Read on build.gradle.kts / build.gradle for Kotlin version, kotlinx-coroutines-*
dependencies, and kotlinx-coroutines-test.Grep for CoroutineScope, GlobalScope, runBlocking, Dispatchers,
viewModelScope, lifecycleScope to locate usage patterns.Each references/ref-*.md file is self-contained (Bad Practice, Recommended, Why, Quick fix). Open the linked file directly — no dependency on repo docs when the skill is installed standalone.
| Topic / Error / Question | Reference file |
|---|---|
| GlobalScope, scope lifetime, "where should I launch?" | ref-1-1-global-scope.md |
| async without await, fire-and-forget with async | ref-1-2-async-without-await.md |
| Breaking structured concurrency, launching in external scope from suspend | ref-1-3-breaking-structured-concurrency.md |
| awaitAll exception propagation, coroutineScope vs supervisorScope for parallel tasks | ref-1-4-awaitall-exception-propagation.md |
| coroutineScope { launch { } } as last line, "wait vs don't wait" | ref-2-1-launch-last-line-coroutine-scope.md |
| runBlocking inside suspend, blocking in coroutines | ref-2-2-runblocking-in-suspend.md |
| Blocking I/O on Default/Main, wrong Dispatchers for I/O | ref-3-1-blocking-wrong-dispatchers.md |
| Main-safe suspend functions, suspend blocking caller thread, ANR | ref-3-2-main-safe-suspend.md |
| Dispatchers.Unconfined in production | ref-3-2-dispatchers-unconfined.md |
| Job() / SupervisorJob() passed to launch/async/withContext | ref-3-3-job-context-builders.md |
| Injecting Dispatchers, hardcoded dispatcher, flaky tests, testability | ref-3-5-inject-dispatchers.md |
| Cancellation in loops, long loops not responding to cancel | ref-4-1-cancellation-intensive-loops.md |
| Periodic / repeating work, polling, zombie coroutine, infinite loop without isActive | ref-4-2-periodic-repeating-work.md |
| Swallowing CancellationException, catch Exception and cancel | ref-4-2-swallowing-cancellation-exception.md |
| Suspend in finally, cleanup that needs to suspend | ref-4-3-suspend-cleanup-noncancellable.md |
| Reusing scope after cancel(), cancelChildren vs cancel | ref-4-4-reusing-cancelled-scope.md |
| withTimeout scope cancellation, TimeoutCancellationException, withTimeoutOrNull | ref-4-6-withtimeout-scope-cancellation.md |
| withTimeout resource cleanup, resource leak on timeout, finally, NonCancellable | ref-4-7-withtimeout-resource-cleanup.md |
| SupervisorJob() in a single builder | ref-5-1-supervisor-job-single-builder.md |
| CancellationException for domain errors (e.g. UserNotFound) | ref-5-2-cancellation-exception-domain-errors.md |
| CoroutineExceptionHandler vs async, exceptions in async stored in Deferred until await() | ref-5-3-exception-handler-async.md |
| Slow tests, real delay() in tests | ref-6-1-slow-tests-real-delays.md |
| Uncontrolled fire-and-forget in tests, can't wait in tests | ref-6-2-uncontrolled-fire-and-forget-tests.md |
| Dispatchers.Main in tests, setMain/resetMain, CI flaky tests | ref-6-3-setmain-resetmain.md |
| Channel not closed, manual Channel without close() | ref-7-1-channel-close.md |
| consumeEach with multiple consumers | ref-7-2-consume-each-multiple-consumers.md |
| Architecture, layers (Data/Domain/Presentation), suspend vs callbacks | ref-8-architecture-patterns.md |
| Lifecycle-aware Flow collection (Android), repeatOnLifecycle, flowWithLifecycle | ref-8-2-lifecycle-aware-flow.md |
| Blocking in flow { }, Thread.sleep in flow, flowOn | ref-9-1-flow-blocking-call.md |
| Cold vs hot flows, StateFlow, SharedFlow, shareIn, stateIn, collect | ref-9-2-cold-vs-hot-flows.md |
| collectLatest semantics, cancels previous block, search vs complete work | ref-9-3-collect-latest.md |
| SharedFlow configuration, replay, extraBufferCapacity, onBufferOverflow, backpressure | ref-9-4-shared-flow-configuration.md |
| suspendCoroutine without cancellation, callback bridge | ref-101-interop-suspend-cancellable.md |
| callbackFlow without awaitClose, listener leak | ref-102-interop-callbackflow-awaitclose.md |
| channelFlow vs callbackFlow, wrong flow builder | ref-103-interop-channelflow-callbackflow.md |
| Future.get in coroutine, CompletableFuture blocking | ref-104-interop-future-get.md |
| MutableStateFlow exposed publicly, UDF violation | ref-95-flow-mutable-exposed.md |
| Flow chain missing catch, launchIn/collect without catch | ref-96-flow-missing-catch.md |
| runBlocking in tests, runTest virtual time | ref-64-test-runtest.md |
| Dispatchers.IO in commonMain, KMP crash | ref-111-kmp-dispatchers-io.md |
| runBlocking in commonMain, KMP deadlock | ref-112-kmp-runblocking.md |
| collectAsState without lifecycle, Compose background collection | ref-83-compose-collect-lifecycle.md |
| rememberCoroutineScope for init, LaunchedEffect | ref-84-compose-remember-scope-init.md |
| Side effect in composable body, recomposition | ref-85-compose-side-effect.md |
| Hardcoded Dispatchers in class, testability | ref-65-test-hardcoded-dispatcher.md |
| advanceUntilIdle missing in runTest, flaky test | ref-66-test-not-completed.md |
| flatMapLatest vs merge vs concat, Flow operator choice | ref-910-flow-flatmap-choice.md |
| SharedFlow one-shot events, Channel for navigation | ref-911-flow-oneshot-events.md |
| synchronized in coroutine, Mutex | ref-121-concur-synchronized.md |
| Sequential async await, wasted Deferred | ref-15-concur-sequential-async.md |
| stateIn Eagerly on ViewModel, WhileSubscribed | ref-97-flow-statein-eagerly.md |
| launchIn GlobalScope, unstructured Flow collection | ref-98-flow-launchin-unstructured.md |
| Side effect in map operator, onEach for effects | ref-99-flow-sideeffect-map.md |
| MDC not propagated, MDCContext | ref-37-backend-mdc.md |
| Blocking JDBC in backend coroutine, Dispatchers.IO | ref-131-backend-blocking.md |
| Missing CoroutineName, debugging | ref-141-debug-coroutine-name.md |
| Redundant nested withContext, same dispatcher twice | ref-36-concur-redundant-withcontext.md |
| MainScope without cancel, KMP presenter leak | ref-113-kmp-mainscope-without-cancel.md |
| Shared mutable state in launch, race on ArrayList | ref-122-concur-shared-mutable-state.md |
| withTimeout cancels parent scope, use withTimeoutOrNull | ref-4-6-withtimeout-scope-cancellation.md |
| Job in builder, SupervisorJob as argument | ref-3-3-job-context-builders.md |
| External scope launch from suspend, structured concurrency break | ref-1-3-breaking-structured-concurrency.md |
| Inline CoroutineScope, scope(CoroutineScope(...)) | ref-1-3-breaking-structured-concurrency.md |
| Scope reuse after cancel, launch after scope.cancel() | ref-4-4-reusing-cancelled-scope.md |
| ViewModelScope leak, collect without lifecycle | ref-8-2-lifecycle-aware-flow.md |
launch – Fire-and-forget or UI-triggered work that does not return a value
// Use for: Work that does not need a result; lifecycle-bound to scope
viewModelScope.launch {
updateUI(loadData())
}
async / await – Parallel work when you need a return value
// Use for: Deferred result; always await (or awaitAll) to preserve structure
coroutineScope {
val a = async { fetchA() }
val b = async { fetchB() }
combine(a.await(), b.await())
}
coroutineScope – Structured child work inside a suspend function
// Use for: Subtasks that must complete or cancel with the current scope
suspend fun loadAll() = coroutineScope {
val one = async { loadOne() }
val two = async { loadTwo() }
Pair(one.await(), two.await())
}
withContext – Switch dispatcher or run cleanup (e.g. NonCancellable)
// Use for: Blocking I/O off Main/Default; cleanup in finally
withContext(Dispatchers.IO) { readFile(path) }
withContext(NonCancellable) { db.close() }
supervisorScope – Children do not cancel each other on failure
// Use for: Independent child jobs (e.g. multiple UI updates)
supervisorScope {
launch { updateA() }
launch { updateB() }
}
produce – Channel that closes when the coroutine ends
// Use for: Single producer; automatic close when scope completes
fun CoroutineScope.flowFromApi() = produce {
for (item in api.stream()) send(item)
}
Scenario: Single network request with UI update
viewModelScope.launch {
val data = withContext(Dispatchers.IO) { repo.fetch() }
updateUI(data)
}
Scenario: Multiple parallel requests
coroutineScope {
val users = async { repo.getUsers() }
val posts = async { repo.getPosts() }
show(users.await(), posts.await())
}
Scenario: Cancellation-friendly loop
for (i in list) {
yield() // or ensureActive()
process(i)
}
coroutineScope + async/launch inside suspend; avoid
launching in external scope from suspend unless documented. For independent tasks, use
supervisorScope + awaitAll so one failure does not cancel siblings.launch. Always call await() on
every Deferred; exceptions are only thrown at await(), not caught by CoroutineExceptionHandler.withContext(IO) internally). Inject
dispatchers for testability.while(isActive) + delay for
repeating work; use yield/ensureActive in long loops; use withContext(NonCancellable) for
suspend cleanup in finally. Prefer withTimeoutOrNull; ensure resources are cleaned up on
timeout.runTest, TestDispatcher, advanceTimeBy; avoid real delay()
in tests. Replace Dispatchers.Main with setMain/resetMain for reliable CI tests.produce; if using Channel manually, document when close() is called;
one consumer per channel with for (x in channel).flow { } non-blocking (use flowOn); use StateFlow for state, SharedFlow
for events with explicit buffer/overflow config; use collectLatest only when cancelling
in-progress work is intentional.repeatOnLifecycle(STARTED) or flowWithLifecycle
so collection stops when UI is in background.Structure every code-review or refactor response as:
For conceptual-only questions, skip erroneous/optimized snippets and keep analysis and explanation.