AI-powered crash analysis is now available on all plans — including Free.Read the crash analysis guide

Kotlin Multiplatform Crash Reporting: Complete Setup Guide

NFNourin Mahfuj Finick··8 min read

Kotlin Multiplatform (KMP) crash reporting presents unique challenges that even experienced mobile developers often underestimate. Unlike single-platform apps where crash handlers follow well-documented paths, KMP applications must bridge the gap between shared Kotlin logic and two fundamentally different native crash ecosystems. This guide walks through everything you need to implement robust crash reporting in your KMP projects, from architectural decisions to production deployment.

Why KMP Crash Reporting Is Different

Kotlin Multiplatform lets you share business logic across Android and iOS, but when an exception occurs, the path to capture it depends entirely on where the crash originated. A NullPointerException thrown in your shared module behaves differently on Android (JVM/ART) than on iOS (Kotlin/Native compiled via LLVM). The stack trace format, signal handling, and even the concept of "uncaught exception" vary between platforms.

This means you cannot simply drop a single crash reporting SDK into your project and expect it to work across targets. Instead, you need an architecture that uses Kotlin's expect/actual mechanism to define a common crash reporting interface while delegating to platform-native handlers. According to the JetBrains 2025 Developer Ecosystem Survey, over 35% of Kotlin developers now target KMP for production apps, yet dedicated crash reporting documentation remains sparse — making this guide especially timely for teams shipping KMP apps in 2026.

Understanding Crash Sources in KMP Applications

Before writing any crash handler code, you need to understand where crashes originate in a KMP codebase. Crashes fall into three categories:

1. Shared Module Kotlin Exceptions

These are standard Kotlin exceptions (NullPointerException, IllegalArgumentException, IndexOutOfBoundsException) that occur inside your commonMain source set. On Android, they surface as regular JVM exceptions with familiar stack traces. On iOS, Kotlin/Native throws them as Objective-C exceptions with Kotlin-to-native stack frame mappings. The crash reporting tool must handle both representations.

2. Platform-Specific Native Crashes

Even in a KMP app, platform code in androidMain or iosMain can trigger native crashes:

  • Android: ANR (Application Not Responding) dialogs from blocked main threads, native SIGSEGV signals from JNI code, and OutOfMemoryError conditions
  • iOS: EXC_BAD_ACCESS from memory corruption, SIGABRT from failed assertions, and watchdog terminations for hung apps

These native-level crashes require platform-specific signal handlers because Kotlin's exception mechanisms never fire.

3. Kotlin/Native Concurrency Crashes

Kotlin/Native's memory model can produce IncorrectDereferenceException and FreezingException when shared mutable state is accessed improperly across threads. The new Kotlin/Native memory manager has improved this, but concurrency crashes still rank among the top KMP pain points according to the 2025 KMP Community Survey.

Setting Up a Unified Crash Handler Using expect/actual

The core pattern for KMP crash reporting uses expect/actual declarations to define a common interface backed by platform-specific implementations. Here is the architecture:

Step 1: Define the common interface in commonMain

// commonMain/kotlin/com/example/crash/CrashReporter.kt
expect class CrashReporter {
    fun initialize()
    fun logBreadcrumb(message: String, category: String = "general")
    fun recordException(throwable: Throwable, metadata: Map<String, String> = emptyMap())
    fun setUserIdentifier(userId: String)
}

Step 2: Implement on Android using Thread.setDefaultUncaughtExceptionHandler

// androidMain/kotlin/com/example/crash/CrashReporter.android.kt
actual class CrashReporter {
    private val breadcrumbs = mutableListOf<String>()
    private var defaultHandler: Thread.UncaughtExceptionHandler? = null
 
    actual fun initialize() {
        defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
        Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
            val report = buildCrashReport(thread, throwable)
            sendToReportingService(report)
            defaultHandler?.uncaughtException(thread, throwable)
        }
    }
 
    actual fun logBreadcrumb(message: String, category: String) {
        breadcrumbs.add("[${category}] $message")
    }
 
    actual fun recordException(throwable: Throwable,
                               metadata: Map<String, String>) {
        val report = buildExceptionReport(throwable, metadata)
        sendToReportingService(report)
    }
 
    actual fun setUserIdentifier(userId: String) {
        // Attach user ID to subsequent crash reports
    }
}

Step 3: Implement on iOS using NSSetUncaughtExceptionHandler and signal handlers

// iosMain/kotlin/com/example/crash/CrashReporter.ios.kt
import platform.Foundation.NSSetUncaughtExceptionHandler
import platform.Foundation.NSException
import kotlinx.cinterop.*
 
actual class CrashReporter {
    actual fun initialize() {
        NSSetUncaughtExceptionHandler { exception ->
            val reason = exception?.reason ?: "Unknown"
            val name = exception?.name ?: "Unknown"
            val crashData = buildIosCrashReport(name, reason)
            sendToReportingService(crashData)
        }
        // Also register signal handlers for SIGABRT, SIGSEGV, SIGBUS
        setupSignalHandlers()
    }
    // ... additional methods
}

This expect/actual pattern is the foundation that every KMP crash reporting strategy builds upon. It ensures your shared module never directly references platform APIs while giving you full control over how crashes are captured on each target.

Integrating Crash Reporting Tools with KMP

Several crash reporting tools now offer varying levels of KMP support. Here is how the landscape looks in 2026:

Sentry KMP SDK

Sentry provides a first-party KMP SDK that supports Android, iOS, and JVM targets. It wraps the native Sentry Android and Sentry Cocoa SDKs behind a Kotlin API available in commonMain. Key features include:

  • Automatic breadcrumb capture from shared code
  • Stack trace translation for Kotlin/Native exceptions
  • Performance tracing across platform boundaries
  • Source context upload for Kotlin stack frames

Setup requires adding the Sentry Gradle plugin and initializing from shared code:

// commonMain
Sentry.init { options ->
    options.dsn = "your-dsn-here"
    options.tracesSampleRate = 1.0
    options.environment = if (isDebug) "development" else "production"
}

Firebase Crashlytics with KMP

Firebase Crashlytics does not provide an official KMP SDK, but you can wrap it using expect/actual. The TouchLab KMP Firebase wrapper includes Crashlytics bindings, though community maintenance can lag behind native SDK releases. For teams already invested in Firebase, this approach works but requires more manual wiring.

Bugspulse KMP Integration

Bugspulse provides a privacy-first mobile crash reporting solution that can be integrated with KMP apps through a lightweight HTTP API. Since the Bugspulse SDK operates at the native layer, you can call it from androidMain and iosMain through expect/actual wrappers while keeping all crash reporting initialization in shared code. The privacy-first architecture means no PII is collected by default, which is critical for KMP apps targeting regulated industries like healthcare and fintech — areas where Bugspulse's HIPAA compliance and GDPR-ready session tracking are especially valuable.

For a broader comparison of crash reporting options, see our best mobile crash reporting tools guide.

Handling Platform-Specific Crash Data

One of the trickiest aspects of KMP crash reporting is making sense of crash data that looks different depending on the platform. A Kotlin exception in shared code produces:

  • On Android: A standard Java stack trace with Kotlin line numbers
  • On iOS: A Kotlin/Native stack trace where function names include mangled suffixes like kfun:com.example.MyClass#doWork

Symbolication for Kotlin/Native

Kotlin/Native crash stacks need symbolication to be readable. On iOS, you can upload dSYM files (generated by the Kotlin compiler) to your crash reporting tool. Configure your Gradle build to preserve debug symbols:

// build.gradle.kts
kotlin {
    iosArm64 {
        binaries.framework {
            freeCompilerArgs += listOf("-Xadd-light-debug=enable")
        }
    }
}

This ensures the Kotlin compiler emits debug information that crash reporting tools can use to deobfuscate stack traces. For additional depth on symbolication workflows, refer to our mobile crash stack trace symbolication guide.

Preserving Shared Module Context

When a crash originates in commonMain but surfaces on a platform handler, you lose the shared module execution context unless you explicitly capture it. Use breadcrumbs from shared code to reconstruct the user journey:

// commonMain
fun processPayment(amount: Double) {
    CrashReporter.logBreadcrumb("Payment initiated", "checkout")
    // ... processing logic
    CrashReporter.logBreadcrumb("Payment completed", "checkout")
}

These breadcrumbs appear in crash reports regardless of which platform thread ultimately crashes, giving you a unified timeline across the KMP boundary.

Production Best Practices for KMP Crash Reporting

1. Coroutine Exception Handlers

If your KMP app uses kotlinx.coroutines (which nearly all do), wrap top-level coroutines with CoroutineExceptionHandler:

val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
    CrashReporter.recordException(throwable, mapOf(
        "scope" to "global",
        "thread" to Thread.currentThread().name
    ))
}
 
scope.launch(exceptionHandler) {
    // shared business logic
}

Without this, unhandled coroutine exceptions silently propagate to the platform default handler, often with truncated stack traces. Our mobile app logging best practices guide covers coroutine-aware logging strategies in more detail.

2. Release Build Configuration

ProGuard/R8 on Android and Kotlin/Native compilation on iOS can strip or mangle symbols needed for readable crash reports. Configure your build to preserve critical metadata:

// ProGuard rules for Android
-keepattributes SourceFile,LineNumberTable
-keep class com.example.shared.** { *; }

On iOS, enable bitcode embedding and avoid aggressive stripping of Kotlin runtime symbols. Test your crash reporting in release builds — this is where symbolication failures become apparent.

3. Testing Crash Reporting End-to-End

Verify your pipeline works before shipping to users:

  • Add a debug-only "Trigger Test Crash" button that throws a known exception
  • Confirm the crash appears in your reporting dashboard within 60 seconds
  • Check that breadcrumbs, user identifiers, and platform metadata are all present
  • Test on both Android and iOS devices, including older OS versions
  • Validate that crash-free rate tracking shows accurate data

4. Monitor KMP Framework Updates

Kotlin Multiplatform evolves rapidly. The KMP crash reporting landscape changes with each Kotlin release. Subscribe to the JetBrains KMP blog and test your crash reporting after every Kotlin version bump. The Kotlin/Native compiler, in particular, can change stack frame generation between releases.

Privacy Considerations for KMP Crash Data

Because KMP apps often process data through shared modules before platform-specific rendering, crash reports may inadvertently capture business logic context. Bugspulse's zero-PII architecture ensures that crash data is stripped of personal information before transmission, making it compliant with GDPR, CCPA, and other privacy regulations out of the box. For apps in regulated industries, this eliminates the need for manual data sanitization pipelines.

Conclusion

Setting up Kotlin Multiplatform crash reporting requires thoughtful architecture — but the expect/actual pattern gives you a clean, maintainable foundation. By defining a common crash reporting interface in commonMain and implementing platform-specific handlers in androidMain and iosMain, you get full crash visibility across your entire KMP codebase without coupling shared code to native SDKs.

The KMP crash reporting ecosystem is still maturing, which means teams that invest in robust crash monitoring today gain a significant advantage in stability and developer experience. Whether you choose Sentry's KMP SDK, a custom expect/actual wrapper around Firebase Crashlytics, or Bugspulse's privacy-first HTTP integration, the principles covered in this guide apply universally.

Ready to ship crash-free KMP apps with confidence? Start your free Bugspulse trial and get real-time crash reporting for Android and iOS, all from a single dashboard — no platform-specific SDK juggling required.