September 21, 2026

Kotlin 2.4.20: what changed and how to migrate without breaking your build

Kotlin 2.4.20 ships a Wasm breaking change, a deprecated watchosArm32 target, and Swift export upgrades. Here's what to fix, in order, before you upgrade.

Insight

TwitterLinkedInFacebook

Kotlin 2.4.20 shipped on September 7, 2026, and it contains one change that will actually stop a build: Kotlin/Wasm now throws a compile-time error if a @JsFun declaration calls top-level require(), according to the 2.4.20 release documentation. Everything else in this release is either a default you can't opt out of but don't need to worry about, a deprecation with no near-term deadline, or a genuine improvement that requires no action at all. If you ship Kotlin/Wasm with JS interop, read the next section before you touch the version number. If you don't, skip ahead to the CLI rename and the watchosArm32 deprecation, the two items most likely to slip past a KMP team unnoticed.

What shipped in Kotlin 2.4.20

The release supports Gradle 7.6.3 through 9.7.0, plus later Gradle releases as they land. Beyond the Wasm breaking change, the headline items are: when-expression compilation via invokedynamic graduating to Stable, the watchosArm32 target getting deprecated, the kotlin CLI command getting replaced by kotlinr, and a set of Swift export improvements for KMP teams shipping to iOS. JetBrains' own changelog treats all of this as one flat feature list (JetBrains). It stops being flat the moment you sort it by what actually forces a code change versus what you can ignore. Teams that follow how Flutter ships features faster will recognize the sorting problem: most of 2.4.20 is low-drama, and exactly one item isn't.

The one breaking change: @JsFun and top-level require() in Kotlin/Wasm

If your Kotlin/Wasm code interops with JS by writing something like this:

@JsFun("(path) => require(path)")
external fun loadModule(path: String): JsAny

...it now fails at compile time, per JetBrains' Kotlin/Wasm interop guidance. Calling top-level require() from inside a @JsFun string was always a workaround, and Kotlin 2.4.20 closes it. You have two ways to fix it. Move the import to @JsModule and reference the module as an external declaration instead of calling require() inline, or replace the call with an import() expression, adding a webpack-ignore comment if your bundler tries to statically analyze it.

This only touches Kotlin/Wasm projects that use this specific interop pattern. JVM, Android, iOS and general KMP shared code aren't affected. If you don't ship a Wasm target, skip straight to the CLI section below.

invokedynamic for when expressions is now Stable

when-expression compilation via invokedynamic moves from experimental, where it landed in Kotlin 2.2.20, to Stable in 2.4.20, and it's on by default for JVM 21+ targets, according to JetBrains' notes on the invokedynamic change. There's no flag to set and no migration step. If your minimum JVM target sits below 21, this doesn't apply yet. If it's 21 or higher, your when expressions compile differently now, and you probably won't notice, unless you run bytecode-sensitive tooling like a coverage instrumenter or a bytecode manipulation library. In that case, one CI run on JVM 21+ is worth the ten minutes.

watchosArm32 is deprecated (KMP/iOS teams, read this)

watchosArm32 is deprecated in 2.4.20, with removal planned for Kotlin 2.5.0, per the watchosArm32 deprecation notice. There's no calendar date attached to that. The release notes tie the deprecation to Apple dropping 32-bit watchOS targets and to Xcode 27 compatibility, not to a fixed timeline, so treat "2.5.0" as "the next major version," not a deadline on your calendar.

This is the item most likely to slip past a KMP team, because a deprecation warning buried in a build.gradle.kts target list is easy to ignore until the target disappears for real. Open your KMP target list now, find watchosArm32, and drop it while it's still a choice rather than a forced fix later. Most teams targeting current Apple hardware haven't shipped to 32-bit watchOS in years, so this is usually a one-line deletion, not a migration. For the broader case behind running Kotlin across platforms in the first place, see why teams are adopting Kotlin Multiplatform.

The kotlin CLI runner is being replaced by kotlinr

The kotlin command still runs scripts and REPL sessions the way it always has, but it now prints a warning and points you to kotlinr instead. The reason is a naming collision: the Kotlin Toolchain ships its own kotlin command, and JetBrains is renaming the older CLI runner so the two stop stepping on each other. Nothing breaks today. Scripts, CI jobs, Makefiles and shell aliases that invoke kotlin keep working. But every one of those invocations now prints a warning in your build logs, and warnings that get ignored for months tend to hide the one that actually matters later.

How to upgrade to Kotlin 2.4.20

1. Audit your Kotlin/Wasm interop for @JsFun and require()

Grep your Wasm source for @JsFun declarations that call require() at the top level. This is the only change in the release that fails a build outright, so fix it before you bump the Kotlin plugin version, not after.

2. Check your Gradle version

Confirm your project runs Gradle 7.6.3 through 9.7.0, or a later supported release, before you bump the Kotlin Gradle plugin. An unsupported Gradle version produces more confusing failures than any of the actual Kotlin changes do.

3. Drop watchosArm32 from your KMP target list

Not urgent, since removal isn't scheduled until 2.5.0, but handle it in the same pull request while you're already touching build config. There's no reason to leave a deprecated target sitting in a file you're editing anyway.

4. Rename kotlin CLI invocations to kotlinr

Update CI pipelines, Makefiles, local shell aliases and any onboarding docs that tell contributors to run kotlin. This is a find-and-replace job, not an engineering task.

5. Bump the Kotlin version and run your full test suite

The invokedynamic change is on by default and low risk, but it's still a change to how bytecode gets generated. Run CI once on JVM 21+ before calling the upgrade done, especially if you rely on coverage tools or bytecode manipulation libraries that could be sensitive to how when expressions compile now.

What KMP teams get on the Swift interop side

The genuinely useful part of this release sits on the Swift export side. Sealed Kotlin class hierarchies now generate a sealedType() method that maps to a Swift enum, so Xcode gets exhaustive switch statements with autocompletion over Kotlin sealed classes instead of a flat class hierarchy developers have to pattern-match by hand, according to the Swift export release notes. Separately, the same release notes cover the assembleSharedXCFramework Gradle task, which now auto-generates a Package.swift file for XCFramework builds that depend on SwiftPM packages, removing a manual publishing step teams have been scripting around on their own.

Neither change is dramatic by itself, but together they're the clearest sign yet that Swift export interop is a real priority for JetBrains, not an afterthought behind the JVM and Android targets. If you're hiring iOS developers onto a KMP-adjacent team, this is the release where the Swift side of that job got noticeably less painful.

FAQ

Do I need to remove watchosArm32 right now?

No. It's deprecated in 2.4.20, but removal is planned for Kotlin 2.5.0 with no calendar date attached. Drop it from your target list whenever convenient, ideally the next time you're already editing your build.gradle.kts.

Does the invokedynamic change require any code changes?

No. It's Stable and on by default for JVM 21+ targets. If your minimum target sits below JVM 21, it doesn't apply to you. The only reason to look twice is if you run tooling that reads bytecode directly, such as a coverage instrumenter.

What if I'm not using Kotlin/Wasm? Does the require() change affect me?

No. The @JsFun and top-level require() compile error only fires in Kotlin/Wasm projects using that specific JS interop pattern. JVM, Android, iOS and general KMP shared code targets aren't touched by it.


Author Image

HighCircl Editorial Team

The HighCircl editorial team writes about hiring software engineers, nearshore development, and engineering team building. Our articles draw on direct experience sourcing and placing senior developers across Poland, Hungary, Slovakia, Serbia, Slovenia, Romania, and Spain — and on candid conversations with the CTOs and engineering leads who hire them.

HighCircl is a nearshore engineering network that delivers matched candidate shortlists in 72 hours. Every piece of content we publish is informed by real engagement data: actual developer rates, real hiring timelines, and what separates engineering teams that scale cleanly from those that stall.

Take Me to the Experts

Access our network of industry-leading software engineers.

Start Now