Skill v1.0.1
currentAutomated scan100/100+62 new
version: "1.0.1" name: kotlin-coroutines-skill description: "Expert guidance on Kotlin Coroutines and structured concurrency. Use when developers write or review async Kotlin code and need help ensuring correctness, safety, and testability of coroutine-based implementations." license: MIT metadata: version: "3.0.0"
Kotlin Coroutines
Overview
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.
Agent Behavior Contract (Follow These Rules)
- Identify the practice or error from the user's code or question (e.g. GlobalScope,
runBlocking in suspend, swallowing CancellationException) and open the corresponding reference from the Triage table in references/.
- Apply the strict rules below in every response. Do not suggest or leave code that violates
them.
- Respond in the required format: Analysis → Erroneous code → Optimized code → Explanation. If
the user only asks a conceptual question (no code), skip erroneous/optimized snippets and focus on analysis and explanation.
- Do not recommend
GlobalScopein production. Use framework scopes (viewModelScope,
lifecycleScope, rememberCoroutineScope), injected scopes, or local scopes ( coroutineScope { }, withContext { }). If an external scope is required, justify and document it.
- Use
asynconly when a return value is needed; ifawait()is never called, uselaunch.
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.
- Never use
runBlockinginside 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.
- Use explicit Dispatchers:
Dispatchers.Defaultfor 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).
- Never pass
Job()orSupervisorJob()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.
- Cancellation handling (apply all):
- Never swallow
CancellationException; rethrow it in catch blocks. - Do not use
CancellationExceptionfor domain errors; use normal exceptions instead. - In long loops and repeating/polling work, add
yield(),ensureActive(), orwhile (isActive)
with delay(interval) so the coroutine responds to cancellation.
- For suspend calls in
finally, usewithContext(NonCancellable) { }. - Do not reuse a scope after
scope.cancel(); usecoroutineContext.job.cancelChildren()to
stop only children while keeping the scope alive.
- Prefer
withTimeoutOrNulloverwithTimeoutto avoid unintentionally cancelling the parent
scope. If using withTimeout, catch TimeoutCancellationException explicitly. Always ensure resources opened inside withTimeout are cleaned up in finally.
- Exception handling:
- Uncaught exceptions in
launchpropagate toCoroutineExceptionHandler; inasync, the
exception is stored in the Deferred and only thrown on await(). Always call await() on async blocks to avoid silently losing exceptions.
- Use
CoroutineExceptionHandlerat scope level forlaunchuncaught exceptions.
- In tests use
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.
- Prefer
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 best practices:
- Keep
flow { }builder non-blocking; useflowOn(Dispatchers.IO)or suspend APIs. - Use
StateFlowfor shared UI state (replays last value); useSharedFlowfor events with
explicit replay, extraBufferCapacity, and onBufferOverflow configuration.
- Use
collectLatestonly when cancelling in-progress work is intentional (e.g. search); use
collect when each item must be processed to completion.
- On Android, collect flows with
repeatOnLifecycle(Lifecycle.State.STARTED)or
flowWithLifecycle to stop collection when the UI goes to background.
- When several practices apply (e.g. GlobalScope + wrong Dispatchers), use each relevant reference
and combine the fixes in one optimized snippet.
Recommended Tools for Analysis
When analyzing Kotlin projects for coroutine issues:
- Project settings
- Use
Readonbuild.gradle.kts/build.gradlefor Kotlin version,kotlinx-coroutines-*
dependencies, and kotlinx-coroutines-test.
- Use
GrepforCoroutineScope,GlobalScope,runBlocking,Dispatchers,
viewModelScope, lifecycleScope to locate usage patterns.
- Scope and lifecycle
- Identify whether code runs on Android (viewModelScope, lifecycleScope), KMP, or plain JVM to
recommend the right scope.
Triage-First Playbook (Topic / Error → Reference)
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 |
Core Patterns Reference
When to Use Each Coroutine Tool
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 scopeviewModelScope.launch {updateUI(loadData())}
async / await – Parallel work when you need a return value
// Use for: Deferred result; always await (or awaitAll) to preserve structurecoroutineScope {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 scopesuspend 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 finallywithContext(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 completesfun CoroutineScope.flowFromApi() = produce {for (item in api.stream()) send(item)}
Common Scenarios
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)}
Best Practices Summary
- Prefer structured concurrency – Use
coroutineScope+async/launchinside suspend; avoid
launching in external scope from suspend unless documented. For independent tasks, use supervisorScope + awaitAll so one failure does not cancel siblings.
- Use the right scope – Framework scopes (viewModelScope, lifecycleScope) or injected scope;
never GlobalScope in production.
- Use async only when you need a result – Otherwise use
launch. Always callawait()on
every Deferred; exceptions are only thrown at await(), not caught by CoroutineExceptionHandler.
- Use explicit, injected Dispatchers – IO for blocking I/O, Default for CPU, Main for UI; never
block on Default/Main. Make suspend functions main-safe (withContext(IO) internally). Inject dispatchers for testability.
- Respect cancellation – Rethrow CancellationException; use
while(isActive)+delayfor
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.
- Do not misuse CancellationException – Use normal exceptions for domain errors.
- Test with virtual time –
runTest,TestDispatcher,advanceTimeBy; avoid realdelay()
in tests. Replace Dispatchers.Main with setMain/resetMain for reliable CI tests.
- Channels – Prefer
produce; if using Channel manually, document whenclose()is called;
one consumer per channel with for (x in channel).
- Flow – Keep
flow { }non-blocking (useflowOn); useStateFlowfor state,SharedFlow
for events with explicit buffer/overflow config; use collectLatest only when cancelling in-progress work is intentional.
- Android lifecycle – Collect flows with
repeatOnLifecycle(STARTED)orflowWithLifecycle
so collection stops when UI is in background.
Output Format (Required for Code Review / Refactor)
Structure every code-review or refactor response as:
- Problem Analysis – Short description of what is wrong (e.g. scope lifetime, dispatcher,
exception handling) and the risk (leaks, ANRs, flaky tests).
- Erroneous Code – The original or problematic code snippet (clearly labeled).
- Optimized Code – Refactored code that follows the guidelines (structured concurrency,
correct scopes, Dispatchers, exception/cancellation handling).
- Technical Explanation – Why the optimized version is safer or more correct: lifecycle,
cancellation propagation, thread usage, testability.
For conceptual-only questions, skip erroneous/optimized snippets and keep analysis and explanation.
Verification Checklist (When You Change Coroutine Code)
- Confirm which scope is in use (framework, injected, or local) and that it matches lifecycle
expectations.
- After refactors: run tests, especially those that use coroutines or virtual time (see ref-6-*).
- If touching cancellation or cleanup: ensure CancellationException is rethrown and suspend cleanup
uses NonCancellable where needed.
- If touching Dispatchers: ensure no blocking I/O on Default or Main.