This specification is maintained by the AgentRules Working Group. Have enhancements or want to claim maintainership?Submit Specification →
mobilecritical PriorityID: kmp-swift-coroutine-cancellation-invariant

KMP to Swift Interop Structured Concurrency and Coroutine Cancellation Invariant

Enforces strict bidirectional structured concurrency across the Kotlin/Native Objective-C export bridge and Swift modern concurrency runtime. Without explicit cancellation bridging via KMP wrapper objects or SKIE-generated async hooks, Swift Task cancellations fail to propagate to active Kotlin CoroutineScopes, causing orphaned background work, memory leaks, and native iOS thread pool starvation.

Prohibited Anti-Pattern: Directly Consuming Kotlin Suspend Functions in Swift Without SKIE or Explicit Job Cancellation Bridges

Export Ready-to-Drop Configuration

Select format below to copy code or download file directly.

---
description: KMP to Swift Interop Structured Concurrency and Coroutine Cancellation Invariant - Enforces strict bidirectional structured concurrency across the Kotlin/Native Objective-C export bridge and Swift modern concurrency runtime. Without explicit cancellation bridging via KMP wrapper objects or SKIE-generated async hooks, Swift Task cancellations fail to propagate to active Kotlin CoroutineScopes, causing orphaned background work, memory leaks, and native iOS thread pool starvation.
globs: iosApp/**/*.swift, shared/src/commonMain/**/*.kt, shared/src/iosMain/**/*.kt, shared/build.gradle.kts
alwaysApply: true
verified_by: "agentrules.org"
license: "CC-BY-4.0"
source: "https://agentrules.org/rules/kmp-swift-coroutine-cancellation-invariant"
---

# KMP to Swift Interop Structured Concurrency and Coroutine Cancellation Invariant

> **Severity**: CRITICAL | **Category**: mobile
> **Anti-Pattern Warning**: Directly Consuming Kotlin Suspend Functions in Swift Without SKIE or Explicit Job Cancellation Bridges

## System Constraints
You are a Principal Mobile Architect specializing in Kotlin Multiplatform (KMP) and native iOS integration. You mandate that any bridge between Kotlin coroutines (suspend functions, Flow) and Swift concurrency (async/await, AsyncSequence) strictly respects structured concurrency. Swift Task cancellation must immediately cancel the corresponding Kotlin Job, and Kotlin CoroutineScope termination must throw a CancellationError in the awaiting Swift caller. You must enforce either compiler-grade interop tooling (such as Touchlab SKIE) or explicit native wrapper patterns with Job-linked withTaskCancellationHandler closures.

## Strict Guidelines
- Engineers MUST ALWAYS enforce bidirectional cancellation propagation between Swift `Task` instances and Kotlin `Job` handles.
- Engineers MUST NOT expose raw Kotlin suspend functions directly to Swift without SKIE or a structured `JobDisposable` interop layer, as default Kotlin/Native Obj-C export ignores Swift `Task.cancel()`.
- Swift client code MUST use `withTaskCancellationHandler` to explicitly invoke `job.cancel(cause:)` when wrapping raw Kotlin callbacks or CoroutineScope executions.
- Kotlin shared code MUST expose cancellation-aware wrappers using `CancellableContinuation` or dedicated `CloseableJob` abstractions if third-party interop plugins (e.g., SKIE) are not enabled in the project.
- Engineers MUST configure SKIE in `build.gradle.kts` with `coroutines.enabled.set(true)` whenever possible to automatically generate seamless async/await cancellation glue code.
- Swift collectors of Kotlin `CommonFlow` or `StateFlow` MUST break collection and release the underlying coroutine when the wrapping Swift Task is cancelled to prevent native thread starvation.

## Bad vs Good Implementations
### Swift Call to Kotlin Suspend Interop Without Cancellation Handling (Leaked Coroutine)

#### Bad Antipattern (AI Common Hallucination)
> **Why it fails**: Default Kotlin/Native to Swift interop translates suspend functions into Swift async methods using completion handlers. When the Swift Task is cancelled via `activeTask?.cancel()`, Swift does not propagate this signal across the C-Objective-C runtime bridge into Kotlin's Job hierarchy, resulting in leaked HTTP requests, orphaned coroutines, and potential race conditions updating shared state.
```swift
import Shared

// ANTI-PATTERN: Default Kotlin-to-Objective-C export maps suspend fun to Swift async,
// but does NOT attach Swift Task cancellation to the underlying Kotlin Job.
@MainActor
final class ProfileViewModel: ObservableObject {
    private let repository = UserRepository()
    private var activeTask: Task<Void, Never>?

    func loadUserProfile(userId: String) {
        activeTask?.cancel() // Swift Task cancels, but Kotlin coroutine keeps executing!
        activeTask = Task {
            do {
                // The Kotlin suspend function continues burning CPU/network in the background
                let profile = try await repository.fetchUserProfile(id: userId)
                print("Loaded: \(profile.name)")
            } catch {
                print("Error or cancellation ignored: \(error)")
            }
        }
    }
}
```

#### Production Standard (Correct)
> **Standard**: By explicitly trapping cancellation with `withTaskCancellationHandler` or leveraging SKIE's synthesized cancellation hooks, cancelling the Swift Task immediately triggers cancellation in the underlying Kotlin Job hierarchy, freeing network sockets and worker threads on iOS.
```swift
import Shared

// PRODUCTION FIX: Explicit bridge enforcing Swift Task -> Kotlin Job cancellation propagation
@MainActor
final class ProfileViewModel: ObservableObject {
    private let repository = UserRepository()
    private var activeTask: Task<Void, Never>?

    func loadUserProfile(userId: String) {
        activeTask?.cancel()
        activeTask = Task {
            do {
                let profile = try await withTaskCancellationHandler {
                    try await repository.fetchUserProfile(id: userId)
                } onCancel: {
                    // Explicitly signal cancellation to the Kotlin bridge if SKIE is not used,
                    // or rely on SKIE's built-in cancellation hooks.
                    repository.cancelOngoingFetch(id: userId)
                }
                guard !Task.isCancelled else { return }
                print("Loaded: \(profile.name)")
            } catch is CancellationError {
                print("Task cleanly cancelled on Swift and Kotlin side")
            } catch {
                print("Encountered domain error: \(error)")
            }
        }
    }
}
```

### Kotlin Side Cancellable Wrapper for iOS Export

#### Bad Antipattern (AI Common Hallucination)
> **Why it fails**: The iOS caller receives no reference to the launched `Job`. If the native view is dismissed, the native side cannot trigger cancellation, leaving the CoroutineScope running until completion and wasting system resources.
```kotlin
// ANTI-PATTERN: Uncancellable wrapper exposed to iOS
package com.example.shared

import kotlinx.coroutines.*

class UserProfileWrapper(private val scope: CoroutineScope) {
    // Exposes a function that starts a job without returning a cancellation handle
    fun fetchProfile(id: String, onComplete: (UserProfile?, Throwable?) -> Unit) {
        scope.launch {
            try {
                val profile = performHeavyFetch(id)
                onComplete(profile, null)
            } catch (e: Throwable) {
                onComplete(null, e)
            }
        }
    }

    private suspend fun performHeavyFetch(id: String): UserProfile = withContext(Dispatchers.Default) {
        delay(5000)
        UserProfile(id, "John Doe")
    }
}
```

#### Production Standard (Correct)
> **Standard**: Returning a `CancellableHandle` allows Swift wrappers to bind `job.cancel()` directly to `withTaskCancellationHandler` on the native side, guaranteeing that view dismissals immediately abort underlying coroutines.
```kotlin
// PRODUCTION FIX: Exposing a CancellationToken / DisposableJob to Swift Interop
package com.example.shared

import kotlinx.coroutines.*

interface CancellableHandle {
    fun cancel()
}

class UserProfileWrapper(private val scope: CoroutineScope) {
    fun fetchProfile(
        id: String,
        onComplete: (UserProfile?, Throwable?) -> Unit
    ): CancellableHandle {
        val job = scope.launch {
            try {
                val profile = performHeavyFetch(id)
                onComplete(profile, null)
            } catch (e: CancellationException) {
                // Structured concurrency cancellation cleanly swallowed or propagated
                throw e
            } catch (e: Throwable) {
                onComplete(null, e)
            }
        }

        return object : CancellableHandle {
            override fun cancel() {
                job.cancel(CancellationException("Swift Task cancelled execution"))
            }
        }
    }

    private suspend fun performHeavyFetch(id: String): UserProfile = withContext(Dispatchers.Default) {
        delay(5000)
        UserProfile(id, "John Doe")
    }
}
```

Recommended MCP Servers (mcp-serverlist.com)

Equip agent with system tools to verify code for this stack.

Architectural Specification & Invariant Rationale

Architectural Invariant Design: KMP to Swift Interop Structured Concurrency and Coroutine Cancellation Invariant

KMP to Swift Interop Structured Concurrency and Coroutine Cancellation Invariant defines an industrial-grade behavioral standard for mobile engineering. Enforces strict bidirectional structured concurrency across the Kotlin/Native Objective-C export bridge and Swift modern concurrency runtime. Without explicit cancellation bridging via KMP wrapper objects or SKIE-generated async hooks, Swift Task cancellations fail to propagate to active Kotlin CoroutineScopes, causing orphaned background work, memory leaks, and native iOS thread pool starvation. Modern software architectures require strict adherence to deterministic patterns to guarantee high availability, maintainability, and zero security vulnerabilities.

Framework Paradigm Shift & Drift MechanicsLLMs are trained on millions of open-source repositories containing outdated code, legacy idioms, and unpatched antipatterns. When coding assistants generate implementations for this stack, they default to common statistical tokens: Directly Consuming Kotlin Suspend Functions in Swift Without SKIE or Explicit Job Cancellation Bridges Without an explicit system instruction file, developers spend hours debugging silent failures.
Failure Modes & Production Blast Radius

Hazard Analysis: What Breaks Without This Rule

The following failure modes have been cataloged from hundreds of hours of production code reviews where AI assistants produced non-compliant implementations:

Failure ModeAI Symptom (The Anti-Pattern)SeverityProduction Impact & Remedy
AI Hallucination & Deprecation DriftDirectly Consuming Kotlin Suspend Functions in Swift Without SKIE or Explicit Job Cancellation BridgesCritical
Violating this industrial standard introduces regression bugs, security risks, and unexpected runtime failures in production.
Enforce explicit behavioral system directives across all AI coding assistants in your repository.
Silent Architecture InconsistencyAI assistants produce mixed paradigms across files, creating fragmented codebase conventions.Medium
Increases cognitive load during code reviews and leads to maintenance debt across engineering teams.
Pin project conventions in .cursor/rules, CLAUDE.md, and AGENTS.md so all team agents follow identical guardrails.
Architectural Directives in Detail

Step-by-Step Production Directives

Each rule constraint below is injected into the AI agent's system prompt to establish deterministic boundaries:

1

Engineers MUST ALWAYS enforce bidirectional cancellation propagation between Swift `Task` instances and Kotlin `Job` handles.

Engineers MUST ALWAYS enforce bidirectional cancellation propagation between Swift `Task` instances and Kotlin `Job` handles.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
2

Engineers MUST NOT expose raw Kotlin suspend functions directly to Swift without SKIE or a structured `JobDisposable` interop layer, as default Kotlin/Native Obj-C export ignores Swift `Task.cancel()`.

Engineers MUST NOT expose raw Kotlin suspend functions directly to Swift without SKIE or a structured `JobDisposable` interop layer, as default Kotlin/Native Obj-C export ignores Swift `Task.cancel()`.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
3

Swift client code MUST use `withTaskCancellationHandler` to explicitly invoke `job.cancel(cause

Swift client code MUST use `withTaskCancellationHandler` to explicitly invoke `job.cancel(cause:)` when wrapping raw Kotlin callbacks or CoroutineScope executions.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
4

Kotlin shared code MUST expose cancellation-aware wrappers using `CancellableContinuation` or dedicated `CloseableJob` abstractions if third-party interop plugins (e.g., SKIE) are not enabled in the project.

Kotlin shared code MUST expose cancellation-aware wrappers using `CancellableContinuation` or dedicated `CloseableJob` abstractions if third-party interop plugins (e.g., SKIE) are not enabled in the project.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
5

Engineers MUST configure SKIE in `build.gradle.kts` with `coroutines.enabled.set(true)` whenever possible to automatically generate seamless async/await cancellation glue code.

Engineers MUST configure SKIE in `build.gradle.kts` with `coroutines.enabled.set(true)` whenever possible to automatically generate seamless async/await cancellation glue code.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
6

Swift collectors of Kotlin `CommonFlow` or `StateFlow` MUST break collection and release the underlying coroutine when the wrapping Swift Task is cancelled to prevent native thread starvation.

Swift collectors of Kotlin `CommonFlow` or `StateFlow` MUST break collection and release the underlying coroutine when the wrapping Swift Task is cancelled to prevent native thread starvation.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
Technical Questions & Answers

Frequently Asked Technical Questions

Common questions developers face when configuring AI coding assistants for this stack:

Q:Why is KMP to Swift Interop Structured Concurrency and Coroutine Cancellation Invariant critical for AI coding assistants?

Enforces strict bidirectional structured concurrency across the Kotlin/Native Objective-C export bridge and Swift modern concurrency runtime. Without explicit cancellation bridging via KMP wrapper objects or SKIE-generated async hooks, Swift Task cancellations fail to propagate to active Kotlin CoroutineScopes, causing orphaned background work, memory leaks, and native iOS thread pool starvation. Without explicit behavioral boundaries, AI coding assistants (Cursor, Claude Code, Windsurf, Codex) rely on training data averages, which frequently produce deprecated, inefficient, or vulnerable code patterns.

Q:How does adopting this rule prevent AI hallucinations?

This rule explicitly declares what the AI must avoid (Directly Consuming Kotlin Suspend Functions in Swift Without SKIE or Explicit Job Cancellation Bridges) and prescribes verified positive examples. Grounding the LLM with negative and positive pairs eliminates guesswork.

Q:Which AI coding environments are supported?

This specification is multi-platform compatible: Cursor (.cursor/rules/*.mdc & .cursorrules), Anthropic Claude Code (CLAUDE.md), Windsurf (.windsurfrules), Codex / Cline (AGENTS.md), and GitHub Copilot (.github/copilot-instructions.md).

Q:Can I combine this rule with other technology stacks?

Yes. Use the Interactive Stack Builder or download an Agent Pack to merge this rule with other frontend, backend, database, security, and DevOps rules without syntax collision.

Open Specification · CC BY 4.0·Free for Personal, Team & Commercial In-Repo Use

Citation & Open Source Attribution Guidelines

This specification is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0). You are completely free to adapt and share this rule in your workspace. If citing, including in public AI benchmark datasets, or redistributing, attribution to AgentRules.org is required.

Related MOBILE Rules

View all rules →
1 Rule in Stack

KMP