Kotlin Help

What's new in Kotlin 2.2.20-RC2

Released: September 1, 2025

The Kotlin 2.2.20-RC2 release is out! Here are some details of this EAP release:

IDE support

The Kotlin plugins that support 2.2.20-RC2 are bundled in the latest versions of IntelliJ IDEA and Android Studio. You don't need to update the Kotlin plugin in your IDE. All you need to do is change the Kotlin version to 2.2.20-RC2 in your build scripts.

See Update to a new release for details.

Language

In Kotlin 2.2.20-RC2, you can try out upcoming language features planned for Kotlin 2.3.0, including improved overload resolution when passing lambdas to overloads with suspend function types and support for return statements in expression bodies with explicit return types.

Improved overload resolution for lambdas with suspend function types

Previously, overloading a function with both a regular function type and a suspend function type caused an ambiguity error when passing a lambda. You could work around this error with an explicit type cast, but the compiler incorrectly reported a No cast needed warning:

// Defines two overloads fun transform(block: () -> Int) {} fun transform(block: suspend () -> Int) {} fun test() { // Fails with overload resolution ambiguity transform({ 42 }) // Uses an explicit cast, but compiler incorrectly reports a "No cast needed" warning transform({ 42 } as () -> Int) }

With this change, when you define both a regular and a suspend function type overload, a lambda without a cast resolves to the regular overload. Use the suspend keyword to resolve to the suspend overload explicitly:

// Resolves to transform(() -> Int) transform({ 42 }) // Resolves to transform(suspend () -> Int) transform(suspend { 42 })

This behavior will be enabled by default in Kotlin 2.3.0. To test it now, set your language version to 2.3 using the following compiler option:

-language-version 2.3

Or configure it in your build.gradle(.kts) file:

kotlin { compilerOptions { languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_3) } }

We would appreciate your feedback in our issue tracker, YouTrack.

Support for return statements in expression bodies with explicit return types

Previously, using return in an expression body caused a compiler error because it could cause the function's return type to be inferred as Nothing.

fun example() = return 42 // Error: Returns are prohibited for functions with an expression body

With this change, you can now use return in expression bodies as long as the return type is written explicitly:

// Specifies the return type explicitly fun getDisplayNameOrDefault(userId: String?): String = getDisplayName(userId ?: return "default") // Fails because it doesn't specify the return type explicitly fun getDisplayNameOrDefault(userId: String?) = getDisplayName(userId ?: return "default")

Similarly, return statements inside lambdas and nested expressions in functions with expression bodies used to compile unintentionally. Kotlin now supports these cases as long as the return type is specified explicitly. Cases without an explicit return type will be deprecated in Kotlin 2.3.0:

// Return type isn't explicitly specified, and the return statement is inside a lambda // which will be deprecated fun returnInsideLambda() = run { return 42 } // Return type isn't explicitly specified, and the return statement is inside the initializer // of a local variable, which will be deprecated fun returnInsideIf() = when { else -> { val result = if (someCondition()) return "" else "value" result } }

This behavior will be enabled by default in Kotlin 2.3.0. To test it now, set your language version to 2.3 using the following compiler option:

-language-version 2.3

Or configure it in your build.gradle(.kts) file:

kotlin { compilerOptions { languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_3) } }

We would appreciate your feedback in our issue tracker, YouTrack.

Kotlin/JVM: support invokedynamic with when expressions

In Kotlin 2.2.20-RC2, you can now compile when expressions with invokedynamic. Previously, when expressions with multiple type checks compiled to a long chain of instanceof checks in the bytecode.

Now you can use invokedynamic with when expressions to generate smaller bytecode, similar to the bytecode produced by Java switch statements, when the following conditions are met:

  • All conditions except for else are is or null checks.

  • The expression doesn't contain guard conditions (if).

  • The conditions don't include types that can't be type-checked directly, such as mutable Kotlin collections (MutableList) or function types (kotlin.Function1, kotlin.Function2, and so on).

  • There are at least two conditions besides else.

  • All branches check the same subject of the when expression.

For example:

open class Example class A : Example() class B : Example() class C : Example() fun test(e: Example) = when (e) { // Uses invokedynamic with SwitchBootstraps.typeSwitch is A -> 1 is B -> 2 is C -> 3 else -> 0 }

With the new feature enabled, the when expression in this example compiles to a single invokedynamic type switch instead of multiple instanceof checks.

To enable this feature, compile your Kotlin code with JVM target 21 or above and add the following compiler option:

-Xwhen-expressions=indy

Or add it to the compilerOptions {} block of your build.gradle(.kts) file:

kotlin { compilerOptions { freeCompilerArgs.add("-Xwhen-expressions=indy") } }

This feature is Experimental. If you have any feedback or questions, share them in YouTrack.

Kotlin Multiplatform

Kotlin 2.2.20-RC2 introduces significant changes for Kotlin Multiplatform: Swift export is available by default, there's a new shared source set, and you can try a new approach to managing common dependencies.

Swift export available by default

Kotlin 2.2.20-RC2 introduces experimental support for Swift export. It allows you to export Kotlin sources directly and call Kotlin code from Swift idiomatically, eliminating the need for Objective-C headers.

This should significantly improve multiplatform development for Apple targets. For example, if you have a Kotlin module with top-level functions, Swift export enables clean, module-specific imports, removing the confusing Objective-C underscores and mangled names.

The key features are:

  • Multi-module support. Each Kotlin module is exported as a separate Swift module, simplifying function calls.

  • Package support. Kotlin packages are explicitly preserved during export, avoiding naming conflicts in the generated Swift code.

  • Type aliases. Kotlin type aliases are exported and preserved in Swift, improving readability.

  • Enhanced nullability for primitives. Unlike Objective-C interop, which required boxing types like Int? into wrapper classes like KotlinInt to preserve nullability, Swift export converts nullability information directly.

  • Overloads. You can call Kotlin's overloaded functions in Swift without ambiguity.

  • Flattened package structure. You can translate Kotlin packages into Swift enums, removing the package prefix from generated Swift code.

  • Module name customization. You can customize the resulting Swift module names in the Gradle configuration of your Kotlin project.

How to enable Swift export

The feature is currently Experimental and works only in projects that use direct integration to connect the iOS framework to the Xcode project. This is a standard configuration for Kotlin Multiplatform projects created with the Kotlin Multiplatform plugin in IntelliJ IDEA or through the web wizard.

To try out Swift export, configure your Xcode project:

  1. In Xcode, open the project settings.

  2. On the Build Phases tab, locate the Run Script phase with the embedAndSignAppleFrameworkForXcode task.

  3. Adjust the script to feature the embedSwiftExportForXcode task instead in the run script phase:

    ./gradlew :<Shared module name>:embedSwiftExportForXcode
    Add the Swift export script
  4. Build the project. Swift modules are generated in the build output directory.

The feature is available by default. If you have already enabled it in previous releases, you can now remove kotlin.experimental.swift-export.enabled from your gradle.properties file.

For more information about Swift export, see its README.

Leave feedback

We're planning to expand and gradually stabilize Swift export support in future Kotlin releases. After Kotlin 2.2.20 we'll focus on improving interoperability between Kotlin and Swift, particularly around coroutines and flows.

Support for Swift export is a significant change for Kotlin Multiplatform. We would appreciate your feedback:

Shared source set for js and wasmJs targets

Previously, Kotlin Multiplatform didn't include a shared source set for JavaScript (js) and WebAssembly (wasmJs) web targets by default. To share code between js and wasmJs, you had to manually configure a custom source set or write code in two places, one version for js and another for wasmJs. For example:

// commonMain expect suspend fun readCopiedText(): String // jsMain external interface Navigator { val clipboard: Clipboard } // Different interop in JS and Wasm external interface Clipboard { fun readText(): Promise<String> } external val navigator: Navigator suspend fun readCopiedText(): String { // Different interop in JS and Wasm return navigator.clipboard.readText().await() } // wasmJsMain external interface Navigator { val clipboard: Clipboard } external interface Clipboard { fun readText(): Promise<JsString> } external val navigator: Navigator suspend fun readCopiedText(): String { return navigator.clipboard.readText().await().toString() }

Starting with this release, the Kotlin Gradle plugin adds a new shared source set for web (comprising webMain and webTest) when using the default hierarchy template.

With this change, the web source set becomes a parent of both js and wasmJs source sets. The updated source set hierarchy looks like this:

An example of using the default hierarchy template with web

The new source set allows you to write one piece of code for both the js and wasmJs targets. You can put your shared code in webMain and it automatically works for both targets:

// commonMain expect suspend fun readCopiedText(): String // webMain external interface Navigator { val clipboard: Clipboard } external interface Clipboard { fun readText(): Promise<JsString> } external val navigator: Navigator suspend fun readCopiedText(): String { return navigator.clipboard.readText().await().toString() }

This update simplifies code sharing between the js and wasmJs targets. It is particularly useful in two cases:

  • For library authors who want to add support for both js and wasmJs targets, without duplicating code.

  • For developers building Compose Multiplatform applications that target the Web, enabling cross-compilation to both js and wasmJs targets for wider browser compatibility. Given this fallback mode, when you create a website, it will work on all browsers out of the box: modern browsers use wasmJs, and older browsers use js.

To try this feature, use the default hierarchy template in the kotlin {} block of your build.gradle(.kts) file:

kotlin { js() wasmJs() // Enables the default source set hierarchy, including webMain and webTest applyDefaultHierarchyTemplate() }

Before using the default hierarchy, consider carefully any potential conflicts if you have projects with a custom shared source set or if you renamed the js("web") target. To resolve these conflicts, rename the conflicting source set or target, or don't use the default hierarchy.

Stable cross-platform compilation for Kotlin libraries

Kotlin 2.2.20-RC2 completes an important roadmap item, stabilizing cross-platform compilation for Kotlin libraries.

You can now use any host to produce .klib artifacts for publishing Kotlin libraries. This significantly streamlines the publishing process, particularly for Apple targets that previously required a Mac machine.

The feature is available by default. If you have already enabled cross-compilation with kotlin.native.enableKlibsCrossCompilation=true, you can now remove it from your gradle.properties file.

Unfortunately, a few limitations are still present. You still need to use a Mac machine if:

For more information about the publication of multiplatform libraries, see our documentation.

New approach for declaring common dependencies

To simplify setting up multiplatform projects with Gradle, Kotlin 2.2.20-RC2 now lets you declare common dependencies in the kotlin {} block by using a top-level dependencies {} block. These dependencies behave as if they were declared in the commonMain source set. This feature works similarly to the dependencies block that you use for Kotlin/JVM and Android-only projects, and it's now Experimental in Kotlin Multiplatform. Declaring common dependencies at the project level reduces repetitive configuration across source sets and helps streamline your build setup. You can still add platform-specific dependencies in each source set as needed.

To try this feature, opt in by adding the @OptIn(ExperimentalKotlinGradlePluginApi::class) annotation before the top-level dependencies {} block. For example:

kotlin { @OptIn(ExperimentalKotlinGradlePluginApi::class) dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") } }

We would appreciate your feedback on this feature in YouTrack.

Kotlin/Native

Kotlin 2.2.20-RC2 brings improvements for Kotlin/Native binaries and debugging.

Support for stack canaries in binaries

Starting with 2.2.20-RC2, Kotlin adds support for stack canaries in the resulting Kotlin/Native binaries. As part of stack protection, this security feature protects against stack smashing, mitigating some common application vulnerabilities. Already available in Swift and Objective-C, it's now supported in Kotlin as well.

The implementation of stack protection in Kotlin/Native follows the behavior of the stack protector in Clang.

To enable stack canaries, add the following binary option to your gradle.properties file:

kotlin.native.binary.stackProtector=yes

The property enables the feature for all the Kotlin functions that are vulnerable to stack smashing. Alternative modes are:

  • kotlin.native.binary.stackProtector=strong, which uses a stronger heuristic for the functions vulnerable to stack smashing.

  • kotlin.native.binary.stackProtector=all, which enables stack protectors for all functions.

Note that in some cases, stack protection might come with a performance cost.

Smaller binary size for release binaries

Kotlin 2.2.20-RC2 introduces the smallBinary option that can help you decrease the binary size for release binaries. The new option effectively sets -Oz as the default optimization argument for the compiler during the LLVM compilation phase.

With the smallBinary option enabled, you can make release binaries smaller and improve build time. However, it might affect runtime performance in some cases.

The new feature is currently Experimental. To try it out in your project, add the following binary option to your gradle.properties file:

kotlin.native.binary.smallBinary=true

The Kotlin team is grateful to Troels Lund for his help in implementing this feature.

Improved debugger object summaries

Kotlin/Native now generates clearer object summaries for debugger tools like LLDB and GDB. This improves the readability of the produced debug information and streamlines your debugging experience.

Previously, if you inspected an object such as:

class Point(val x: Int, val y: Int) val point = Point(1, 2)

You'd see limited information, including a pointer to the memory address:

(lldb) v point (ObjHeader *) point = [x: ..., y: ...] (lldb) v point->x (int32_t *) x = 0x0000000100274048

With Kotlin 2.2.20-RC2, the debugger shows richer details, including the actual values:

(lldb) v point (ObjHeader *) point = Point(x=1, y=2) (lldb) v point->x (int32_t) point->x = 1

The Kotlin team is grateful to Nikita Nazarov for his help in implementing this feature.

For more information on debugging in Kotlin/Native, see the documentation.

Kotlin/Wasm

Kotlin/Wasm receives some quality of life improvements, including separated npm dependencies and improved exception handling for JavaScript interop.

Separated npm dependencies

Previously, in your Kotlin/Wasm projects, all npm dependencies were installed together in your project folder. It included both your own dependencies and Kotlin tooling dependencies. These dependencies were also recorded together in your project's lock files (package-lock.json or yarn.lock).

As a result, whenever Kotlin tooling dependencies were updated, you had to update your lock files even if you didn't add or change anything.

Starting from Kotlin 2.2.20-RC2, the Kotlin tooling npm dependencies are installed outside your project. Now, the tooling and the user dependencies have separate directories:

  • Tooling dependencies' directory:

    <kotlin-user-home>/kotlin-npm-tooling/<yarn|npm>/hash/node_modules

  • User dependencies' directory:

    build/wasm/node_modules

Also, the lock files inside the project directory contain only user-defined dependencies.

This improvement keeps your lock files focused only on your own dependencies, helps maintain a cleaner project, and reduces unnecessary changes to your files.

This change is enabled by default for the wasm-js target. The change is not yet implemented for the js target. While there are plans to implement it in future releases, the behavior of the npm dependencies remains the same for the js target in Kotlin 2.2.20-RC2.

Improved exception handling in Kotlin/Wasm and JavaScript interop

Previously, Kotlin had difficulty understanding exceptions (errors) thrown in JavaScript (JS) and crossing over to Kotlin/Wasm code.

In some cases, the issue also occurred in the reverse direction, when an exception was thrown or passed through the Wasm code to JS and wrapped into WebAssembly.Exception without any details. These Kotlin exception handling issues made debugging difficult.

Starting from Kotlin 2.2.20-RC2, the developer experience with exceptions improves in both directions:

  • When exceptions are thrown from JavaScript: you can see more information on Kotlin's side. When such an exception propagates through Kotlin back to JS, it's no longer wrapped into WebAssembly.

  • When exceptions are thrown from Kotlin: they can now be caught on JavaScript's side as JS errors.

The new exception handling works automatically in modern browsers that support the WebAssembly.JSTag feature:

  • Chrome 115+

  • Firefox 129+

  • Safari 18.4+

In older browsers, the exception handling behavior remains unchanged.

Kotlin/JS

Kotlin 2.2.20-RC2 supports using the BigInt type to represent Kotlin's Long type, enabling Long in exported declarations. Additionally, this release adds a DSL function to clean up Node.js arguments.

Usage of BigInt type to represent Kotlin's Long type

Before the ES2020 standard, JavaScript (JS) did not support a primitive type for precise integers larger than 53 bits.

For this reason, Kotlin/JS used to represent Long values (which are 64-bit wide) as JavaScript objects containing two number properties. This custom implementation made interoperability between Kotlin and JavaScript more complex.

Starting with Kotlin 2.2.20-RC2, Kotlin/JS now uses JavaScript's built-in BigInt type to represent Kotlin's Long values when compiling to modern JavaScript (ES2020).

This change enables exporting the Long type to JavaScript, a feature also introduced in 2.2.20-RC2. As a result, this change simplifies the interoperability between Kotlin and JavaScript.

To enable it, add the following compiler option to your build.gradle(.kts) file:

// build.gradle.kts kotlin { js { ... compilerOptions { freeCompilerArgs.add("-Xes-long-as-bigint") } } }

This feature is still Experimental. Please report any problems in our issue tracker, YouTrack.

Usage of Long in exported declarations

Because Kotlin/JS used a custom Long representation, it was difficult to provide a straightforward way to interact with Kotlin's Long from JavaScript. As a result, you couldn't export Kotlin code that used the Long type to JavaScript. This issue affected any code using Long, such as function parameters, class properties, or constructors.

Now that Kotlin's Long type can be compiled to JavaScript's BigInt type, Kotlin/JS supports exporting Long values to JavaScript, simplifying the interoperability between Kotlin and JavaScript code.

To enable this feature:

  1. Allow exporting Long in Kotlin/JS. Add the following compiler argument to the freeCompilerArgs attribute in your build.gradle(.kts) file:

    // build.gradle.kts kotlin { js { ... compilerOptions { freeCompilerArgs.add("-XXLanguage:+JsAllowLongInExportedDeclarations") } } }
  2. Enable the BigInt type. See how to enable it in Usage of BigInt type to represent Kotlin's Long type.

New DSL function for cleaner arguments

When running a Kotlin/JS application with Node.js, the arguments passed to your program (args) used to include:

  • The path to the executable Node.

  • The path to your script.

  • The actual command-line arguments you provided.

However, the expected behavior for args was to include only the command-line arguments. To achieve this, you had to manually skip the first two arguments using the drop() function inside your build.gradle(.kts) file or in your Kotlin code:

fun main(args: Array<String>) { println(args.drop(2).joinToString(", ")) }

This workaround was repetitive, error-prone, and didn't work well when sharing code between platforms.

To fix this issue, Kotlin 2.2.20-RC2 introduces a new DSL function called passCliArgumentsToMainFunction().

With this function, the arguments only include the command-line arguments and exclude the Node and script paths:

fun main(args: Array<String>) { // No need for drop() and only your custom arguments are included println(args.joinToString(", ")) }

This change reduces boilerplate code, avoids mistakes caused by manually dropping arguments, and improves cross-platform compatibility.

To enable this feature, add the following DSL function inside your build.gradle(.kts) file:

kotlin { js { nodejs { passCliArgumentsToMainFunction() } } }

Gradle: new compiler performance metrics in build reports for Kotlin/Native tasks

In Kotlin 1.7.0, we introduced build reports to help track compiler performance. Since then, we've added more metrics to make these reports even more detailed and useful for investigating performance issues.

In Kotlin 2.2.20-RC2, build reports now include compiler performance metrics for Kotlin/Native tasks.

To learn more about build reports and how to configure them, see Enabling build reports.

Maven: support for the Kotlin daemon in the kotlin-maven-plugin

With the introduction of the build tools API in Kotlin 2.2.0, Kotlin 2.2.20-RC2 goes one step further by adding support for the Kotlin daemon in the kotlin-maven-plugin. When using the Kotlin daemon, the Kotlin compiler runs in a separate isolated process, which prevents other Maven plugins from overriding system properties. You can see an example in this YouTrack issue.

Starting with Kotlin 2.2.20-RC2, the Kotlin daemon is used by default. This gives you the added benefit of incremental compilation, which can help speed up your build times. If you want to revert to the previous behavior, opt out by setting the following property in your pom.xml file to false:

<properties> <kotlin.compiler.daemon>false</kotlin.compiler.daemon> </properties>

Kotlin 2.2.20-RC2 also introduces a new jvmArgs property, which you can use to customize the default JVM arguments for the Kotlin daemon. For example, to override the -Xmx and -Xms options, add the following to your pom.xml file:

<properties> <kotlin.compiler.daemon.jvmArgs>Xmx1500m,Xms500m</kotlin.compiler.daemon.jvmArgs> </properties>

Standard library: support for identifying interface types through reflection in Kotlin/JS

Kotlin 2.2.20-RC2 adds the experimental KClass.isInterface property to the Kotlin/JS standard library.

With this property, you can now check whether a class reference represents a Kotlin interface. This brings Kotlin/JS closer to parity with Kotlin/JVM, where you can use KClass.java.isInterface to check if a class represents an interface.

To opt in, use the @OptIn(ExperimentalStdlibApi::class) annotation:

@OptIn(ExperimentalStdlibApi::class) fun inspect(klass: KClass<*>) { // Prints true for interfaces println(klass.isInterface) }

We would appreciate your feedback in our issue tracker, YouTrack.

19 August 2025