Exceptions
A Kotlin function, property accessor, or constructor that throws crosses the bridge as a .NET exception instead of aborting the process. Nothing needs to be written differently in the exported Kotlin to get this: it applies uniformly to every generated call shape.
The exception types come from the shared Xxfast.Kotlin.Native.Interop package, under the Kotlin.Native.Interop namespace. If your .NET project references multiple Kotlin-built packages, an unmapped failure from either can be caught as KotlinException; mapped failures keep their usual .NET base type and implement IKotlinException.
Catching a specific exception type
A Kotlin exception maps to the closest .NET type when it is one of the classes below or a subclass of one, so class KennelFullException : IllegalStateException() is caught as InvalidOperationException. The generated type derives from the .NET type and implements IKotlinException; KotlinType still names your concrete Kotlin class.
Kotlin | C# |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The first matching row wins, so NumberFormatException is a FormatException and not an ArgumentException, and a CancellationException is an OperationCanceledException and not an InvalidOperationException. Anything else, including IndexOutOfBoundsException and a user-defined exception that extends none of these, arrives as the shared KotlinException.
The exception to the table is a cancelled C# callback that escapes Kotlin uncaught: it reaches C# as the original OperationCanceledException type, see Lambdas and callbacks.
The IOException row exists only in a library that has kotlinx-io on its compile classpath, for example through Ktor. It applies on every route. That includes a Kotlin suspend lambda you invoke from C# and a StateFlow you collect, held or awaited, and a Kotlin implementation of a C# interface that C# calls: an IOException thrown there arrives as KotlinIOException like any other. To see which exception crossed and how it was mapped, set NUGET_INTEROP_TRACE=1; see Tracing exceptions.
Exception messages
Message is the Kotlin message verbatim. When the Kotlin exception has no message, Message is the Kotlin class's full name instead, so throw NullPointerException() reads as kotlin.NullPointerException rather than a blank or generic text. ToString() adds a Kotlin type: <name> line above the Kotlin stack trace.
Catching any Kotlin exception
Every generated exception type, mapped or not, implements IKotlinException:
KotlinStackTrace holds Kotlin frames only: the Kotlin exception's toString() line, then one at line per frame, from where it was thrown down to the bridge's kn_<hex>_... export function. The .NET and OS frames of your own process below that are not included; the C# exception's StackTrace has the .NET half. The raw native stack has dozens of those frames, printed as 0x0 + <address> or as a nearby symbol with an offset in the millions, which reads like a C++ crash. The bridge cuts them before the trace crosses:
A suspend or Flow body throws on a Kotlin worker thread, so its trace has no kn_<hex>_... frame and ends at the last Kotlin frame instead. If a trace on macOS or Linux still shows 0x0 or huge-offset frames, report it.
Use it as a catch filter to handle any Kotlin exception the same way, mapped or not:
Exception.InnerException follows Kotlin's cause chain, one exception per link, each mapped (or falling back to KotlinException) independently. Each exception carries its own trace, and the outer KotlinStackTrace has no Caused by: section, so read causes through InnerException (Kotlin Suppressed exceptions do not cross at all):
Where this applies
The same error channel carries a thrown exception out of a property getter or setter, a primary, secondary, or generic class constructor, a value class's constructor (see Value classes), and a data class's generated Copy(), which re-runs the constructor's init validation. A suspend function propagates the exception through the returned Task the ordinary C# async way; see Coroutines and Flow.
A method with a List/Map/Set parameter releases its temporary collection handle whether the call throws or returns; see Collections. For an exception thrown out of a Kotlin implementation of a C#-declared interface, called back from C#, see The bridgeable subset. The reverse direction, a C# lambda or callback that throws while Kotlin is calling it, is a separate channel; see Exceptions from a callback.
Throwable values
A Throwable, Throwable? or stdlib subtype that Kotlin returns reads in C# as a plain, unthrown Exception (or Exception?), built with the same type mapping and cause chain a thrown exception gets. It is something you inspect, not something you catch. This applies to a return from any exported type (a value class's own member included) or a top-level function, a property getter, a List element (nullable included) and a Map value. A Result<Throwable> return binds too, with its TryX twin. It also applies in async and callback positions: a suspend result, a Flow element, a StateFlow element on a property or method, a lambda parameter, an add* listener parameter, and a parameter of an interface you implement in C#.
Each read builds a new Exception, so compare Message and type, not references. There is nothing to dispose. A class in your module that extends Exception but is not exported binds the same way: it arrives mapped through its nearest mapped base, with KotlinType naming the class. An exported exception class stays an ordinary exported class instead.
The async and callback positions read the same way:
Passing an exception to Kotlin
A parameter, var property, constructor parameter or data class copy declared exactly Throwable, Exception or RuntimeException, nullable or not, accepts any System.Exception. So do a suspend or Flow parameter, a List or MutableList element, a Map value, a lambda result, and the result of an interface method you implement in C#:
An interface you implement in C# crosses both ways: Kotlin hands your method an unthrown exception, and what your method returns reaches Kotlin as a NugetManagedException:
Two classes are named NugetManagedException. A reverse throw (a bound C# member throws) is the internal one in your module, which nativeMain code can catch and whose message is the raw .NET message. A fault in a C# callback you passed to Kotlin is a different class in the runtime library, which nativeMain cannot name: catch Exception there, or name it from a per-target source set. Its message is "<type>: <message>".
Because a data class's constructor and copy now accept an exception, a class such as data class Issue56Failure(val reason: String, val error: Throwable?, val fatal: Throwable) is constructible from C#:
What is not bound
A declaration with one of these shapes is left out of the generated C# with a build warning that names it:
an input declared narrower than
RuntimeException, such asIllegalStateExceptionor your own exception class, at any of the positions above; declare itExceptionto bind it. A C# class cannot implement an interface whose method result is declared that narrow; Kotlin implementations of it are unaffecteda
Set<Throwable>or aMapwithThrowablekeys, as a parameter or a result. Each crossing builds a newExceptionthat compares by reference, so no lookup could match; use aLista
StateFlow<Throwable>returned from asuspendfunction, and anyMutableStateFlow<Throwable>; expose a read-onlyStateFlowproperty or method insteada
Throwableextension receiver and a value class overThrowablea lambda result narrower than
RuntimeException, on a plain,suspend, stored, property or returned lambda, which follows the lambda rules (a(Throwable?) -> Unitlambda and asuspendlambda returningThrowablebind)
Result return values
A kotlin.Result<T> return from an ordinary (non-suspend) function lowers to T: success returns the payload, and Result.failure(e) throws exactly as throw e would, mapped the same way as any other exception. Result<Unit> binds as void, not Unit.
Telling a failure from a thrown exception
The throwing member cannot tell a modelled Result.failure apart from an exception the Kotlin body threw: both surface as the same mapped KotlinException subtype. For that, every such member also gets a TryX twin that follows the .NET Try pattern. It returns false for a Result.failure, hands back the mapped exception through failure, and still throws for an exception the body threw. A Result<Unit> twin has only failure.
The twin is generated on class, object, companion, top-level, extension, sealed, interface and abstract members, and overrides copy the modifier. On an interface it is a default interface method that calls the throwing member, so a C# class implementing the interface does not write it. Three things to know:
A
Result.failurethat Kotlin built withrunCatchingaround a programming error returnsfalsetoo:TryXreports what the Kotlin function modelled.If
TryXwould share a name with a property, constant or nested type of the same type, with the type itself, with an extension property on the same receiver, or with a member the type inherits from a base declared in your module, the twin is dropped with the warningSKIPPED_RESULT_TRY_COLLISIONand the throwing member stays. Rename one of them, or give one a@CSharpName, to getTryXback. If one twin in an override chain is dropped, every twin in the chain is dropped, with one warning each. A base from a dependency module cannot be checked, so its twin is kept. An enum's member property is an extension method, so the twin stays beside it.There is no twin on a
new(covariant) sealed arm member, which inherits the base's, on an interface member whose payload mentions a variant type parameter (fun next(): Result<T>onI<out T>; implementing classes keep theirs), or on asuspend fun(which binds as aTask<T>, see below). None of these warns. The twin carries no default parameter values and no XML doc.
An ordinary return position and a suspend fun return are supported. Result<T> at a property, parameter, collection element, Flow<Result<T>> or value-class-own-member position keeps its existing skip, as does a Result<T> whose payload has no return shape of its own (a sealed base, Flow).
A suspend fun returning Result<T> binds as Task<T> on every suspend owner, and a failure faults the task; see suspend fun returning Result<T>.