Skip to content
cccskills
FreePublished on GitHub

platform-coroutines-structured-concurrency

Write and review Kotlin coroutine code for IntelliJ modules by checking who owns each scope, how cancellation and failures propagate, and whether long-running work cooperates with cancellation. Get concrete guidance for safer lifecycle handling and focused fixes.

OriginalShow the original description

Write or review IntelliJ coroutine scope ownership and cancellation.

JetBrains/intellij-community20,664 stars(GitHub 20,664 + cccskills 0)

Run score Not tested

See how to install

Good for

  • Review coroutine scope ownership
  • Find swallowed cancellation or leaked jobs
  • Check cancellation behavior in Flow collectors

Files included (1)

  • SKILL.md11.3 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

<!-- Generated by community/.ai/render-guides.mjs; edit community/.agents/skills/platform-coroutines-structured-concurrency/SKILL.md -->

Coroutines: structured concurrency

Keep cancellation, failures, and lifetimes predictable in Kotlin coroutine code across any IntelliJ-based product module. Three rules govern every decision:

  1. Hierarchy — which scope owns this coroutine, and when is that scope cancelled.
  2. Propagation — what a cancellation or failure does to parents, siblings, and children.
  3. Don't be creative — prefer standard primitives (coroutineScope, supervisorScope, a lifecycle-bound scope) over hand-rolled scope wiring, the most common source of leaks.

Reach for this skill when changing suspend functions, launch/async, CoroutineScope construction, cancellation handling, or Flow collection — and when diagnosing leaked coroutines, uncancellable loops, swallowed cancellation, GlobalScope, launch-in-init, detached scopes, invokeOnCompletion, ProcessCanceledException, blocking/progress bridges, ModalityState context capture, or promise/callback cancellation bridges.

Related skills: platform-deep-dives (coroutine internals, dispatchers, read/write actions); kotlin-ui-swing-component-architecture (Swing UI and EDT ownership).

Core review loop

  1. Name the owning CoroutineScope and the exact moment it is cancelled (Disposable disposal, service teardown, explicit cancel()). If you cannot name that moment, that is the finding.
  2. Check long-running, CPU-bound, blocking, or looped coroutine bodies (and Flow collectors) for a cooperative cancellation checkpoint — do not flag short bodies that already suspend.
  3. Check every catch on the path for a swallowed CancellationException / ProcessCanceledException.
  4. Check any invokeOnCompletion, GlobalScope, manual CoroutineScope(...), or launch in init against the rules below, then classify (severity table) and propose the smallest structural fix.

Scope ownership and lifecycle

  • Never use GlobalScope for feature work — it is never cancelled, so its coroutines outlive the component that started them.

  • Prefer an injected or platform lifecycle scope (project/service scope, or the scope handed to you). A standalone CoroutineScope(context) with no Job in the context gets a fresh root Job() not linked to any parent — nothing cancels or awaits it, so it leaks:

    // Wrong: detached root scope; cleanup depends on a manual cancel() that is easy to miss.
    val scope = CoroutineScope(someContextElement.asContextElement())
    
    // Right: child of an owning scope; cancelled structurally with its parent.
    val scope = owner.coroutineScope.childScope("MyFeature", someContextElement.asContextElement())
    
  • If a manual scope is unavoidable, either make it a child of an owning scope, or register the owning Disposable (cancel the scope in dispose()) before launching any work, and verify that registration actually runs. Disposal-only cancellation with no parent link is fragile — a missed dispose() leaks everything the scope launched.

  • launch in init {} — depends on what cancels the scope. If the object owns a scope tied to its own Disposer registration, don't launch in init: the coroutine can run (touching a half-built this) before registration completes and leak on failure — expose start() and call it after Disposer.register. A @Service with an injected CoroutineScope is the exception: that scope is cancelled with the service's container (app/project/plugin) by the platform — not via your dispose(), and no Disposable is needed solely to cancel the injected scope — so a service self-starting workers from init is acceptable. Just keep the constructor cheap and don't read not-yet-initialized state.

    // Owned scope, cancelled via your own Disposer registration:
    init { scope.launch { observe() } }          // Wrong — may run before Disposer.register completes
    fun start() { scope.launch { observe() } }   // Right — after Disposer.register(owner, this)
    // @Service(private val scope: CoroutineScope) — self-start from init is fine; scope is platform-managed.
    
  • Prefer the shortest lifecycle that consumes the work; use an app/project service scope only when the result is genuinely owned for that whole lifetime.

  • Do not store per-submission UI context in a long-lived scope. Context elements such as current ModalityState must be captured at the submit site, not at service/object construction. Avoid CoroutineScope(ModalityState.defaultModalityState().asContextElement()); prefer scope.launch(currentModality.asContextElement()) { ... } when modality belongs to that operation. Modality dispatch semantics belong to kotlin-ui-swing-component-architecture / platform-deep-dives.

Job hierarchy and failure propagation

  • Cancelling a parent cancels all children recursively; a child failing with anything other than CancellationException cancels its parent and siblings. SupervisorJob/supervisorScope opts child failure out of cancelling the parent; parent cancellation still cancels supervised children.
  • A parent that finished its block is completing, not done, until every child completes.
  • coroutineScope { } is all-or-nothing (one failure fails the scope and rethrows); supervisorScope { } is only for genuinely independent children.

Cooperative cancellation

  • A loop/CPU-bound body with no suspension point is not cancellable and hangs on cancel(). Add a suspension point (delay, yield), ensureActive(), or checkCanceled() in progress-aware code.
  • Callback APIs: bridge with suspendCancellableCoroutine (not suspendCoroutine) and release the resource in invokeOnCancellation.
  • Blocking APIs: use a blocking-appropriate context (Dispatchers.IO) or, for progress-aware blocking code, coroutineToIndicator { indicator -> ... }. Pair cancellation with an explicit interrupt/close strategy; coroutine cancellation alone does not abort an in-progress blocking call. Do not wrap blocking code in blockingContext: deprecated since 2024.2 because context is installed implicitly (ReplaceWith("action()")).

Exception and failure propagation

  • runCatching { launch { ... } } does not catch the child's failure. The child fails its parent scope out-of-band while launch returns normally, so try/runCatching sees nothing. To isolate a child, use supervisorScope and handle the failure inside the child.

  • The same trap applies to async: try/runCatching around async { } does not contain a later failure — the exception surfaces at await(), and an unsupervised failure may already have cancelled the parent before you await.

  • Never swallow cancellation. runCatching and bare catch (e: Throwable) catch CancellationException too; swallowing it breaks structured concurrency and can hang cancellation. ProcessCanceledException is a CancellationException subtype, so catching cancellation covers it too. Rethrow cancellation first:

    try {
      body()
    } catch (c: CancellationException) {   // also covers ProcessCanceledException (a subtype)
      throw c                              // never swallow cancellation
    } catch (x: Throwable) {
      handle(x)
    }
    
  • When bridging to a non-coroutine promise/callback, settle it before rethrowing cancellation. If a coroutine owns an AsyncPromise or a callback result, cancellation can skip the normal result path and leave external awaiters pending. In catch (c: CancellationException), complete the external primitive (setError(c) / cancel) before throw c. Plain Deferred does not need this — cancellation completes it.

  • Do not report a caught CancellationException as a user-visible error, even if you rethrow it.

Don't cancel yourself

Do not call cancel() on the coroutine you are currently running in — it does not stop execution immediately (only at the next suspension point) and poisons the surrounding scope. Return early or throw CancellationException. A shared suspend fun must never cancel its caller's job.

invokeOnCompletion

Prefer to avoid it. Three failure modes make it a trap:

  1. Runs concurrently, unordered, on an unspecified thread — not guaranteed on the EDT or after your surrounding code. A check-then-act on a shared field races; use AtomicReference.compareAndSet.
  2. Retained until the job completes — registering on a long-lived job (or in a loop) accumulates handlers and leaks. Only register on short-lived jobs that actually finish.
  3. Must not throw — exceptions from handlers are reported through coroutine exception handling/logging, not to the caller waiting for completion. Keep the body to trivial, non-failing bookkeeping.

Preferred alternative — do completion work as the last step inside the coroutine:

scope.launch {
  try { doWork() }
  finally { withContext(NonCancellable + Dispatchers.EDT) { onFinished() } }
}

Use NonCancellable only for small, bounded cleanup that must run even after cancellation — never wrap substantial work in it, and keep UI cleanup lifecycle/disposal-guarded (do not touch a disposed component). Cancelling an already-completed Job is a no-op, so "clear my job handle on completion" bookkeeping is often unnecessary — verify it changes behavior before adding it.

Severity defaults

Adjust for actual impact.

PatternDefaultSmallest fix
GlobalScope for feature workCriticalchild of a lifecycle scope
Detached CoroutineScope never cancelledCriticalchildScope of an owning scope
Swallowed CancellationException/ProcessCanceledExceptionCriticalrethrow cancellation before catch (Throwable)
Non-cooperative infinite/long loopCriticaladd ensureActive()/yield/suspension point
runCatching/try around launch/async to contain failureCriticalsupervisorScope + handle inside child
launch in init with a scope tied to own Disposer registrationMajorstart() after registration
Manual scope where childScope fits; scope not tied to lifecycleMajorinject/childScope; register Disposable before launch
invokeOnCompletion mutating shared state unsynchronized or that can throwMajorcompareAndSet, or clean up in coroutine finally
Self-cancellation (cancel() on current job)Majorreturn / throw CancellationException
Promise/callback bridge left pending on cancellationMajorcomplete/cancel external primitive before rethrow
Unnecessary invokeOnCompletion bookkeepingMinorremove it
blockingContext wrapper (deprecated 2024.2; context now implicit)Minordelete the wrapper
Cancellation reported as a user-visible errorMinorrethrow silently
supervisorScope/SupervisorJob where a plain scope sufficesMinoruse coroutineScope

Further reading

For deep internals see platform-deep-dives (coroutine notebooks: cancellation model, context propagation) and the ultimate coroutine docs docs/IntelliJ-Platform/4_man/Kotlin-Coroutines/, especially 9_Gotchas-and-practices/ and 8_UI-EDT-Dispatchers.md.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

actions

FreeSummary

Implement or update IntelliJ `AnAction` classes and their `plugin.xml` registrations. Keep labels and descriptions in message bundles, configure optional icons, and launch suspending work through the event coroutine scope so synchronous action handling does not freeze the IDE.

  • Adding a new IntelliJ action
  • Updating an action's registration or icon
  • Moving action labels into a message bundle
JetBrains/intellij-community20.7KUpdated Oct 26, 2026

Move IntelliJ JPS test modules to Bazel and diagnose failures that occur only under Bazel. The guide helps trace missing dependencies, plugins, test data, and runfiles, then verify the generated target and migrated tests.

  • Migrating a JPS test module to Bazel
  • Diagnosing a test failure that occurs only in Bazel
  • Checking Bazel dependencies and test data wiring
JetBrains/intellij-community20.7KUpdated Oct 26, 2026

code-style

FreeSummary

Write or review Kotlin and Java code in line with IntelliJ project conventions. The guidance covers language choice, formatting, sealed types, threading annotations, and safe handling of control flow exceptions so changes fit the surrounding codebase.

  • Writing a new Kotlin file that follows IntelliJ conventions
  • Reviewing Java code and checking its style guidance
  • Checking formatting, sealed-type handling, or exception handling
JetBrains/intellij-community20.7KUpdated Oct 26, 2026

commits

FreeSummary

Create, amend, or rename IntelliJ commits with messages that follow the project’s ticket and label formats. Keep each commit focused, explain the reason and decision in the body when needed, and pass the message safely through the shell or PowerShell.

  • Writing a message for a change linked to a YouTrack ticket
  • Preparing a commit message for tests, cleanup, or documentation
  • Amending or renaming an existing IntelliJ commit
JetBrains/intellij-community20.7KUpdated Oct 26, 2026

Compare how Python type checkers interpret a code snippet or file. The script runs selected tools, including ty, pyrefly, basedpyright, mypy, and zuban, then produces a Markdown report with verdicts, timings, and each checker's diagnostic output.

  • Comparing type inference for a Python snippet
  • Checking whether a Python expression should be a type error
  • Reviewing differences among Python type checkers
JetBrains/intellij-community20.7KUpdated Oct 26, 2026

Shape Compose Desktop UI code by keeping composables focused on rendering and placing application decisions in presenters or coordinators. Get guidance on state ownership, effects, reusable component APIs, lazy-list keys, animation, and performance for individual UI changes.

  • Separating screen rendering from application decisions
  • Choosing which UI state belongs in composition
  • Structuring effects and coroutine work
JetBrains/intellij-community20.7KUpdated Oct 26, 2026

See all skills by JetBrains

Report a problem with this skill