Exceptions
Every generated bridge call has an out IntPtr error parameter (or the object-handle equivalent for constructors). When a Kotlin function throws, the native call packs the exception into that out-parameter instead of aborting the process; the generated C# checks it after every call and throws the corresponding .NET exception. The mechanism is uniform across functions, property getters/setters, and constructors. Anything that can throw in Kotlin gets an error slot.
Kotlin | C# | Notes |
|---|
thrown exception | KotlinException
| synchronous propagation, ADR-023/ADR-024 |
stack trace | KotlinStackTrace property
| ADR-027 |
e.cause
| InnerException
| cause chain, ADR-028 |
IllegalArgumentException etc.
| ArgumentException etc.
| core exceptions mapped via IKotlinException, ADR-029 |
property getter/setter throws | propagated | ADR-030 |
constructor / init throws | propagated | primary, secondary, data class copy(), generic + value class constructors, ADR-031–ADR-035 |
KotlinException and IKotlinException
Every generated exception type implements IKotlinException, carrying the original fully-qualified Kotlin class name and the Kotlin-side stack trace:
public interface IKotlinException
{
string KotlinType { get; }
string KotlinStackTrace { get; }
}
public class KotlinException : Exception, IKotlinException
{
public string KotlinType { get; }
public string KotlinStackTrace { get; }
public KotlinException(string kotlinType, string message, string kotlinStackTrace,
Exception? innerException = null) : base(message, innerException)
{
KotlinType = kotlinType;
KotlinStackTrace = kotlinStackTrace;
}
public override string ToString()
{
return base.ToString()
+ Environment.NewLine + " ---> Kotlin stack trace:"
+ Environment.NewLine + KotlinStackTrace
+ Environment.NewLine + " --- End of Kotlin stack trace ---";
}
}
Core exception type mapping
A fixed set of Kotlin stdlib exceptions map to the closest .NET analog, as a sealed class implementing IKotlinException and inheriting the matching .NET base type. Anything not in this table falls back to the base KotlinException.
Kotlin | C# |
|---|
IllegalArgumentException
| KotlinArgumentException : ArgumentException
|
IllegalStateException, NoSuchElementException, ConcurrentModificationException
| KotlinInvalidOperationException : InvalidOperationException
|
UnsupportedOperationException
| KotlinNotSupportedException : NotSupportedException
|
ClassCastException
| KotlinInvalidCastException : InvalidCastException
|
ArithmeticException
| KotlinArithmeticException : ArithmeticException
|
NumberFormatException
| KotlinFormatException : FormatException
|
NullPointerException, IndexOutOfBoundsException, user-defined
| KotlinException (fallback)
|
Kotlin
Sample throw sites, from test-library/src/nativeMain/kotlin/.../cat/MappedExceptions.kt:
fun checkOreoWeight(grams: Int): String {
if (grams > 0) throw IllegalArgumentException("Oreo is on a diet, $grams g treat is too much")
return "Mylo accepted ${-grams} g of kibble gracefully"
}
fun activateLaserPointer(catName: String): String {
if (catName == "Oreo") error("Cannot play: Oreo is asleep")
return "$catName chased the red dot enthusiastically"
}
A cause chain, from CauseExceptions.kt:
fun groomCat(catName: String): String {
if (catName == "Oreo") {
val root = RuntimeException("clippers jammed")
val mid = IllegalStateException("grooming aborted", root)
throw IllegalArgumentException("Oreo's grooming failed", mid)
}
return "$catName is fluffy"
}
A throwing property setter/getter, from PropertyExceptions.kt:
class TreatJar(initial: Int) {
var treatCount: Int = initial
set(value) {
require(value >= 0) { "Treat count cannot be negative" }
field = value
}
}
class SnackBowl {
private val snacks: MutableList<String> = mutableListOf()
val nextSnack: String
get() {
check(!isEmpty) { "Snack bowl is empty — Mylo ate everything" }
return snacks.first()
}
}
A throwing constructor, from ConstructorExceptions.kt:
class Kitten(val name: String, val age: Int) {
init {
require(age >= 0) { "Kitten age cannot be negative" }
}
}
class CatLitter(val brand: String, val weightKg: Int) {
init {
require(weightKg > 0) { "Litter weight must be positive" }
}
constructor(brand: String, bags: Int, perBag: Int) : this(brand, bags * perBag)
}
Generated C#
Every bridge call checks the error out-parameter and converts it via NugetErrorNative.BuildException:
public static string checkOreoWeight(int grams)
{
IntPtr nativeResult = checkOreoWeight_native(grams, out IntPtr error);
if (error != IntPtr.Zero)
{
throw NugetErrorNative.BuildException(error);
}
return Marshal.PtrToStringUTF8(nativeResult)!;
}
The same pattern appears on property setters (TreatJar.TreatCount), getters (SnackBowl.NextSnack), and constructors (Kitten(string, int)), all shown throughout this project's Interop.cs. CatId's value-class constructor routes through a private CreateChecked helper that does the same check (see Value classes).
Using it from C#
Type mapping, from IntegrationTests/ExceptionTypeMappingTests.cs:
[Fact]
public void Oreo_OnDiet_IsExactType_KotlinArgumentException()
{
var ex = Assert.ThrowsAny<ArgumentException>(
() => MappedExceptions.checkOreoWeight(10));
Assert.IsType<KotlinArgumentException>(ex);
}
[Fact]
public void Oreo_ToyBehindSofa_NullPointer_IsBaseKotlinException()
{
// NullPointerException is NOT mapped — .NET reserves NullReferenceException for the CLR
var ex = Assert.Throws<KotlinException>(
() => MappedExceptions.retrieveCatToy("Oreo"));
Assert.IsType<KotlinException>(ex);
}
[Fact]
public void CatchAll_ViaIKotlinException_Guard_WorksForMappedType()
{
// The idiom that works for ANY Kotlin exception, mapped or unmapped
Exception? caught = null;
try
{
MappedExceptions.checkOreoWeight(10);
}
catch (Exception ex) when (ex is IKotlinException)
{
caught = ex;
}
Assert.NotNull(caught);
var ke = (IKotlinException)caught;
Assert.Equal("kotlin.IllegalArgumentException", ke.KotlinType);
Assert.NotEmpty(ke.KotlinStackTrace);
}
Cause chain, from IntegrationTests/ExceptionCauseTests.cs:
[Fact]
public void Oreo_GroomingFailed_DeepChain_RootCause_IsBaseKotlinException()
{
// RuntimeException is NOT mapped — stays base KotlinException
var ex = Assert.ThrowsAny<ArgumentException>(
() => CauseExceptions.groomCat("Oreo"));
var mid = (InvalidOperationException)ex.InnerException!;
Assert.IsType<KotlinException>(mid.InnerException);
}
Property propagation, from IntegrationTests/PropertyExceptionPropagationTests.cs:
[Fact]
public void TreatJar_NegativeTreatCount_SetterThrowsArgumentException()
{
using var jar = new TreatJar(5);
Assert.ThrowsAny<ArgumentException>(
() => jar.TreatCount = -1);
}
[Fact]
public void SnackBowl_EmptyBowl_GetterThrowsInvalidOperationException()
{
using var bowl = new SnackBowl();
Assert.ThrowsAny<InvalidOperationException>(
() => bowl.NextSnack);
}
Constructor propagation, from IntegrationTests/ConstructorExceptionPropagationTests.cs, including a data class's generated Copy(), which re-runs the same init validation:
[Fact]
public void Kitten_NegativeAge_ConstructorThrowsArgumentException()
{
Assert.ThrowsAny<ArgumentException>(
() => new Kitten("Oreo", -1));
}
[Fact]
public void CatProfile_Copy_NegativeTreatBudget_ThrowsArgumentException()
{
using var profile = new CatProfile("Oreo", 100);
Assert.ThrowsAny<ArgumentException>(
() => profile.Copy("Oreo", -1));
}
Secondary constructors, from IntegrationTests/SecondaryConstructorExceptionTests.cs, exported as separate overloads (catlitter_create/catlitter_create_2) that each propagate independently:
[Fact]
public void CatLitter_SecondaryConstructor_ZeroBags_ThrowsArgumentException()
{
Assert.ThrowsAny<ArgumentException>(() => new CatLitter("Tidy", 0, 5));
}
Async exception propagation, from IntegrationTests/ExceptionPropagationTests.cs:
[Fact]
public async Task OreoOnDiet_ThrowsArgumentException_WithTypeName()
{
var ex = await Assert.ThrowsAnyAsync<ArgumentException>(
() => AsyncExceptions.FetchCatTreatAsync("Oreo"));
var ke = (IKotlinException)ex;
Assert.Equal("kotlin.IllegalArgumentException", ke.KotlinType);
Assert.Equal("Oreo is on a diet!", ex.Message);
}
Last modified: 21 July 2026