When exposing a Kotlin suspend function to Swift via @Throws, how should CancellationException be handled, and why is it special?
answer
- suspend → Swift async/completion that can carry an error
- Cancellation = CancellationException thrown cooperatively
- Never swallow CancellationException; rethrow it
- Cleanup in finally + withContext(NonCancellable)
- Unlisted exceptions still crash, even in suspend
basics
~20 sSuspend functions become Swift async calls with a completion that can deliver an error. You should mark them so cancellation can be reported to Swift; otherwise a cancelled coroutine could crash instead of telling Swift it was cancelled.
solid answer
~40 sKotlin/Native exports a `suspend` function to Swift as an `async`/completion-handler method that can deliver an error. Just like non-suspend functions, only exceptions whose types are in `@Throws` bridge; unlisted ones crash. The subtlety is `kotlinx.coroutines.CancellationException`: when the coroutine is cancelled, the suspend function completes by throwing it. If `CancellationException` is not bridgeable, that throw can become a fatal boundary error. Kotlin/Native has special handling so cancellation is delivered to Swift as an error (mapped toward Swift structured-concurrency cancellation), but you must ensure your `@Throws` set and call style let it propagate as an error rather than crash. Best practice: list the recoverable domain exceptions explicitly, rely on the runtime's cancellation bridging, and keep cleanup in `finally`/`withContext(NonCancellable)` so resources release even when the coroutine is cancelled.
code
kotlin · 10 lines@Throws(ApiException::class)
suspend fun search(q: String): List<Hit> {
try {
return client.query(q) // suspends; cancellation throws CancellationException
} catch (e: CancellationException) {
throw e // MUST rethrow so cancellation propagates to Swift
} finally {
withContext(NonCancellable) { metrics.flush() } // runs even if cancelled
}
}go deeper
Knows suspend functions show up as async in Swift and can report errors.
Knows the unlisted-crash rule still applies and that errors arrive via the completion.
Handles CancellationException correctly (rethrow), uses NonCancellable cleanup, and curates the @Throws set for suspend APIs.
Defines structured-concurrency conventions across the KMP boundary so cancellation and resource cleanup are consistent and crash-free in iOS clients.
## How suspend functions cross to Swift A Kotlin `suspend` function is exported to Objective-C/Swift as an **async method with a completion handler** (Swift surfaces it as `async`). The completion can carry either a result or an **error**, so the `@Throws` machinery still applies: only **listed** exception types are delivered as errors; **unlisted** ones reaching the boundary are fatal. ```kotlin @Throws(ApiException::class, CancellationException::class) suspend fun fetch(id: String): Dto { ... } ``` ## Why CancellationException is special In `kotlinx.coroutines`, cancellation is **cooperative** and signaled by throwing `kotlinx.coroutines.CancellationException` from suspension points. Inside Kotlin this is normal control flow — you must **not** swallow it. But at the **language boundary** it is just another throwable, so if it is not bridgeable it could crash the process when a coroutine is cancelled. Kotlin/Native's interop has **dedicated handling for cancellation**: when the Swift side cancels (or the coroutine is cancelled), the completion is finished with a cancellation error rather than aborting. This lets Swift's structured concurrency observe the cancellation. The mechanism relies on `CancellationException` being recognized as a cancellation signal, so it is delivered as an error, not a crash. ## What you must get right - **Don't catch-and-swallow `CancellationException`** in your Kotlin code; rethrow it. Swallowing it breaks cooperative cancellation and confuses the bridge. - **Resource cleanup** belongs in `finally`; if cleanup itself suspends, wrap it in `withContext(NonCancellable)` so it runs even after cancellation. - **List your recoverable domain exceptions** in `@Throws`; let the runtime handle cancellation delivery. - Anything you do NOT list (e.g. a stray `IllegalStateException`) still crashes — so audit suspend bodies for leaks of unexpected exception types. ```kotlin @Throws(ApiException::class) suspend fun upload(file: File): Url { try { return remote.put(file) // may throw ApiException, or CancellationException on cancel } finally { withContext(NonCancellable) { tmp.delete() } // cleanup survives cancellation } } ``` ## Mental model - Non-suspend `@Throws`: synchronous `NSError**` channel. - suspend `@Throws`: async completion that can carry an error; cancellation maps to Swift cancellation. - The rule "unlisted ⇒ crash" is unchanged for both.
- What happens if you catch CancellationException and return a default value instead of rethrowing?You break cooperative cancellation: the coroutine appears to complete normally, the bridge can't report cancellation to Swift, and parent scope cancellation semantics get corrupted.
- How is a suspend function exported to Swift?As an async method (completion-handler based) that delivers either a result or an error; the @Throws rules for which exceptions bridge still apply.
Cancellation is like a 'stop' note passed up the chain; swallow it and the worker keeps running blindly, which jams the whole assembly line at the border.
saying these in an interview costs you the question
- Swallowing CancellationException and returning a fallback
- Thinking suspend functions bypass the unlisted-crash rule
- Putting cancellable cleanup outside NonCancellable so it never runs
- Believing cancellation always crashes the Swift app
- Not knowing suspend maps to a Swift async/completion API