kotlin-coroutine-review
Coroutine bugs are almost never at compile time and rarely visible in unit tests — they show up as thread-pool starvation, silent exception swallowing, or leaked jobs. This skill runs a structured audit of a specific coroutine-heavy file or module.
Prerequisites
- Target file(s) visible.
- Kotlin version resolvable from
build.gradle/build.gradle.kts. kotlinx-coroutines-coreversion resolvable (behavior differs between 1.6, 1.7, 1.8).
Phase 1 — Anti-pattern sweep
Walk the file and flag each hit against this catalog:
Blocking inside suspend
Thread.sleep(...)in asuspend fun→ usedelay(...)..get()/.await()on aFuture/CompletableFuture→ use the-awaitbridge (kotlinx-coroutines-jdk8).- Any JDBC call in a suspend function on the default dispatcher → wrap with
withContext(Dispatchers.IO). JDBC is blocking. runBlocking { ... }inside a suspend function → almost always a mistake; you're already in a coroutine.
GlobalScope / structured-concurrency violations
GlobalScope.launch { ... }→ leaked coroutine; won't be cancelled by parent. Use an injectedCoroutineScopeorcoroutineScope { ... }.CoroutineScope(Dispatchers.IO).launch { ... }inline → same issue; the scope has no parent.- Any
launch { ... }inside a suspend function whose result the caller depends on → parent may return before child completes. Wrap the block incoroutineScope { ... }or make it a top-levelasync.
Dispatcher misuse
Dispatchers.Defaultfor I/O work → occupies the CPU pool. UseDispatchers.IO.Dispatchers.IOfor CPU-bound work → starves I/O for other consumers. UseDispatchers.Default.newSingleThreadContext(...)in library code → creates and never closes a thread. Use an existing dispatcher or take aCoroutineScopefrom the caller.Dispatchers.Mainused in a non-UI backend service → the main dispatcher isn't installed on the JVM by default; runtime error at first use.
Exception handling
try { ... } catch (e: CancellationException) { ... }that swallows the exception → breaks cancellation propagation. Rethrow, or usetry/finallyfor cleanup.launch { ... }without aCoroutineExceptionHandleron the scope → uncaught exceptions terminate the parent scope (structured concurrency) or crash the process (root scope).async { ... }whose.await()is never called → exception is swallowed untilawait()is invoked.supervisorScope { ... }used where you actually want failure to propagate → sibling coroutines keep running after a failure.
Flow / Channel pitfalls
flow { emit(...) }doing I/O without.flowOn(Dispatchers.IO)→ emissions block the collector's dispatcher.stateIn(scope, WhileSubscribed(), initial)without aWhileSubscribed(5000)timeout → subscription churn on config changes recreates upstream unnecessarily.Channel(UNLIMITED)in a producer without a bounded consumer → memory leak under back-pressure..collectLatest { ... }in a UI where every emission triggers cancellation of in-flight work → intentional if for user typing, dangerous if for RPC calls.
Scope lifecycle
- Spring
@Componentholding aCoroutineScopefield with no cancellation on@PreDestroy→ jobs outlive the bean. - Ktor
Applicationscope leaked into request handlers → request-scoped work runs beyond the request. - Android
viewModelScopein a repository → repository logic tied to a specific viewmodel's lifecycle by accident.
Phase 2 — Version-specific gotchas
Note behaviour differences by kotlinx-coroutines version:
- 1.6.x —
Dispatchers.IO.limitedParallelism(n)limits the shared pool globally. Later releases have per-instance behaviour. - 1.7.x —
runInterruptibleinterop with blocking JVM APIs was rewritten; olderwithContext(IO) { blockingCall() }may not surfaceInterruptedExceptioncorrectly. - 1.8+ —
Flow.filterIsInstanceshort-circuiting semantics changed for empty inputs.
If the file relies on version-specific behaviour, note it.
Phase 3 — Emit findings
Same format as spring-kafka-listener-review:
[<severity>] <category>: <one-line summary>
Where: <file>:<line>
Why it matters: <impact in 1 sentence>
Fix: <concrete code change>
Reference: <kotlinx-coroutines doc / KEEP link if applicable>
Severity ladder:
- must-fix — leaks, silent exception swallowing, cancellation-safety violations
- should-fix — dispatcher misuse, scope lifecycle unclarity
- consider — style, more idiomatic patterns
Non-goals
- Not a Kotlin style linter (ktlint / detekt cover that).
- Does not run the code. Verification of behaviour is the user's next step.
- Does not migrate blocking code to coroutines wholesale — that's a design decision, not a review output.