Artem Olkov
09/01/2026, 7:39 AMArtem Olkov
09/01/2026, 7:39 AM// kt
fun load(): Int
suspend fun load(): Int
// swift
func load() -> Int
func load() async throws -> Int
As you can see, adding suspend currently impacts two distinct traits at once:
1. The function turns asynchronous.
2. The function becomes throwing by default.
Ideally, these would be separate concerns. The suspend modifier should imply async, but it shouldn't automatically determine the throwing status.Artem Olkov
09/01/2026, 7:40 AMfun value(): Int = 5
fun failure(): Int = throw Throwable("hello there")
suspend fun suspendValue(): Int = 5
suspend fun waitForever(): Int = awaitCancellation()Artem Olkov
09/01/2026, 7:40 AM//------------------------ //
1/ Which side should require an Annotation
Since Kotlin functions are unchecked and lack explicit throw declarations, Swift Export is forced to establish a baseline behavior and offer a manual override mechanism.
If used accurately, either approach is capable of generating an identical Swift interface.
Option A - @Throws
Basically this will preserve the current status quo of the Objective-C export model.
This will leave us with the following changes to original code snippet
fun value(): Int = 5
@Throws(Throwable::class)
fun failure(): Int = throw Throwable("hello there")
And the following call site
value() // returns 5
try failure() // throws
In the standard configuration, Kotlin functions are translated as non-throwing. Developers must manually apply the @Throws annotation to specify which APIs should be exported as throwing to the Swift side.
The advantage is a cleaner Swift call site by default. The disadvantage is that forgetting @Throws can turn an escaping Kotlin exception into runtime termination.
Option B - @NonThrowing
This will leave us with the following changes to original code snippet
@NonThrowing
fun value(): Int = 5
fun failure(): Int = throw Throwable("hello there")
And the following callsite
value() // returns 5
try failure() // throws
Unannotated Kotlin functions are exported as throwing. Authors use @NonThrowing for APIs that promise not to expose failures to Swift.
The advantage is that forgetting an annotation only produces an unnecessary try, rather than an uncatchable failure. The disadvantage is that Swift APIs become throwing unless the Kotlin author explicitly opts outArtem Olkov
09/01/2026, 7:41 AM//------------------------ //
2/ Should cancellation always make a suspend function throwing?
Option A: Cancellation always uses the error channel
try await value() // never throws
try await waitForever() // receives CancellationError when cancelled
Every exported suspend function is at least async throws, even when it does not expose any ordinary Kotlin exceptions. This ensures that cancellation can always cross the interop boundary safely
Option B: Allow an explicitly non-throwing async export
await value()
await waitForever() // runtime termination if cancellation crosses the boundary
The Kotlin author can explicitly request an async function without throws. This removes try from the Swift call site, but cancellationāor any other escaping throwableācannot be delivered as a Swift error and therefore terminates at runtime.
Which behavior do you prefer?
1. Always preserve cancellation: every exported suspend function is async throws.
2. Allow an explicit opt-out: an annotation can export a suspend function as async without throws, accepting runtime termination if cancellation or another throwable escapes.Artem Olkov
09/01/2026, 7:41 AM//------------------------ //
3/ What should @Throws say about exception types?
Regardless of which default we choose, we also need to decide what Swift Export should do with the list of exception classes stated in @Throws.
Option A: Require and enforce the list
@Throws(NetworkException::class) fun load(): Data = ...
@Throws(ParseException::class) fun parse(): Model = ...
Only listed exceptions and their subclasses are propagated to Swift. An unlisted exception escaping the function terminates at runtime.
This makes the annotation an explicit failure contract, but Kotlin cannot verify that the list is exhaustive or remains up to date(mirrors current Objective-C export behaviour).
Option B: Make @SwiftThrows as a marker, same as @Throws(Exception::class)
@Throws(Exception::class) fun load(): Data = ...
@SwiftThrows fun parse(): Model = ...
The annotation only controls whether the Swift function is generated with throws. Any uncaught Kotlin Throwable escaping the call is propagated as a Swift Error.
This avoids stale exception lists, but provides no documentation about the expected failures.
Option C: Keep an optional, documentation-only list
// theoretical new syntax, possibly a new annotation
@Throws(NetworkException::class) fun load(): Data = ...
@Throws fun parse(): Model = ...
The listed exceptions describe expected failures, but do not act as a runtime whitelist. Any escaping Kotlin Throwable is still propagated to Swift.
What should @Throws mean?
1. A required and enforced list of exceptions.
2. A marker without an exception list; propagate every escaping throwable.
3. An optional documentation-only list; propagate every escaping throwable regardless of the list.Guilherme Delgado
09/01/2026, 8:02 AM@Throws(RateLimitException::class, CancellationException::class)
suspend fun getSet(setCode: String): Result<List<Card>>
Thatās exactly the kind of āeasy to forgetā annotation staleness you called out elsewhere ā if someone lists their business exceptions but forgets CancellationException, cancellation stops being able to cross the boundary and you get a runtime crash for a concern that has nothing to do with the business logic the author was thinking about. Making cancellation propagation automatic and independent of @Throws removes that footgun entirely.
3 ā leaning toward Option C. An enforced whitelist (A) just moves the āforgot to updateā crash risk one level deeper: even a function the author did remember to annotate can still crash if a new exception type shows up from a transitive dependency. Since Kotlin canāt verify exhaustiveness anyway, Iād rather the list be documentation-only and never gate what actually crosses the boundary ā same runtime behavior as the bare marker (B), but without throwing away useful info for Swift consumers/tooling.
One question this raises: with 1+2 combined, a suspend fun without @Throws is already async throws because of cancellation. In that case, does an unannotated business exception also get to ride that already-open throws channel, or does it still crash even though the signature says throws? If it still crashes, that feels like a sharper footgun than the non-suspend case, since the Swift dev sees throws and reasonably assumes any error is catchable.Filip Wiesner
09/01/2026, 8:03 AMArtem Olkov
09/01/2026, 8:16 AMsuspended to a function forces to add two additional effect handlers on the callsite - try block and async context. So we are wondering, what's best - addition of suspension requires only a single change to the callsite, but adds a footgun, or force user to catch an exception that probably is never thrown.
On the intersection of this question with it's neighbours there are options for introducing new annotations
1. @Cancellable (adds throws, basically equal to @Throws(CancellationException::class))
2. @nonCancellable (removes throws, crashes runtime in case of cancellation)Artem Olkov
09/01/2026, 8:17 AMArtem Olkov
09/01/2026, 8:21 AMOne question this raises: with 1+2 combined, a suspend fun without @Throws is already async throws because of cancellation. In that case, does an unannotated business exception also get to ride that already-open throws channel, or does it still crash even though the signature says throws?That's exactly the problem, yes, If
suspend fun implicitly adds Throws(Cancellation) to it's signature - should explicit Throws override it? Or cancellation should always be added to the list? Or maybe a nonCancellable marker would make more sence?Artem Olkov
09/01/2026, 8:28 AMthrows on the swift side with it? This way a following code would make sence (not exact syntax, but the idea)
sealed interface MyErrors: Throwable("")
object ParseError: MyErrors
object AuthError: MyErrors
object ServerError: MyErrors
@Throws(ParseError::class, AuthError::class)
fun doWork() = TODO()
// swift
func doWork() throws(MyErrors) { /**/ }
do { doWork() }
catch let e as MyErrors {
switch e.sealed {
case .parse: /**/
case .auth: /**/
case .server: /**/
}
}
But with this throwing a IlligalStateException would crash the runtimeGuilherme Delgado
09/01/2026, 8:29 AMsuspend fun implicitly adds Throws(Cancellation) to itās signature - should explicit Throws override it? Or cancellation should always be added to the list? Or maybe a nonCancellable marker would make more sence?
For me, cancellation should always get merged into the list, not overridable by an explicit @Throws. If we let @Throws replace it instead of merging, weāre basically back to square one ā someone writes @Throws(BusinessError::class), forgets cancellation exists, and boom, same crash risk we just decided to remove in Q2.
The whole point was to take this off peopleās mental checklist, so an override that quietly undoes that feels like a trap. š
@NonCancellable Iām a bit wary of, not against it. Itās Q2's Option B in disguise ā an opt-out that trusts the authorās function really canāt be cancelled. But the compiler canāt check that, same problem as the @Throws list in Q3: if the authorās wrong, or a suspension point sneaks in later, itās a silent crash with no warning.Guilherme Delgado
09/01/2026, 8:36 AMWhat do you think about a possibility of calculating the last common ancestor of the listed exceptions and markingLove the ideia š and what if, instead of crashing on the mismatch, the generated Swift error type had a synthesized catch-all case e.g:on the swift side with it? This way a following code would make sence (not exact syntax, but the idea)throws
.other(Error)
For anything outside the declared hierarchy? Youād keep the nice switch for modeled cases, but unexpected stuff falls into .other instead of terminating. Best of both: good UX when errors are well-modeled, no crash risk when theyāre not.
This only pays off if your exceptions already share one sealed hierarchy. If not, the LCA collapses toward Throwable and you lose the switch granularity anyway ā so Iād treat it as an opt-in enhancement for well-modeled APIs, not a replacement for the ānever crash on unlisted exceptionsā default.Artem Olkov
09/01/2026, 8:38 AMIf not, the LCA collapses toward ThrowableYeah, that's our concern also - that such an approach would collapse into
Throwable in 90% cases and that's not very usefulGuilherme Delgado
09/01/2026, 8:43 AMFranƧois
09/01/2026, 11:48 AMArtem Olkov
09/01/2026, 11:53 AMcan we translate a kotlin typed exception with into swift typed throws ?Only if we calculate LCA - as Swift allows only one type to be listed š But there are nuances: 1. for example - we expect that most cases would be calculated into Exception::class 2. Throwing something that does not inherit the resulted LCA would force us to crash the runtime 3. Such LCA on suspend functions would always be either Exception or CancellationException
FranƧois
09/01/2026, 12:02 PMArtem Olkov
09/01/2026, 12:12 PMstill the Swift-style way to handle errors properly from swiftLet me cite an original proposal of those š
even with the addition of typed throws to Swift, untypedis better for most scenarios. Consider typed throws only in the following circumstances:throws
1. In code that stays within a module or package where you always want to handle the error, so it's purely an implementation detail and it is plausible to handle the error.
2. In generic code that never produces its own errors, but only passes through errors that come from user components. The standard library contains a number of constructs like this, whether they arefunctions likerethrowsor are capturing amaptype like inFailureorTask.Result
3. In dependency-free code that is meant to be used in a constrained environment (e.g., Embedded Swift) or cannot allocate memory, and will only ever produce its own errors.None of those are a good match for our case.
Waldemar Kornewald
09/10/2026, 9:57 AMEddie
09/14/2026, 6:54 PMthrows by having the Kotlin function return some version of a Result type? In other words, Kotlin returns Result<SucessType, ErrorType> and that translates to func myFunc() throws(ErrorType) -> SuccessType in Swift?
This doesnāt solve the issue with suspend functions but I think it would solve some of the problems with the @Throws annotation that @Artem Olkov mentioned at the top of this thread: you canāt return a failure type that you didnāt explicitly declare and you are forced to make all thrown errors conform to a superclass.