Kotlin can be compiled to JavaScript. This allows us to distribute our libraries or common code to use in JavaScript/TypeScript projects. However, their approach to concurrency is very different. JavaScript uses async functions and promises, while Kotlin uses suspending functions and flows. In this article we will explore how to make Kotlin Coroutines API JavaScript-friendly, and how to use JavaScript from Kotlin Coroutines.
Converting suspending functions to JavaScript async functions
In JavaScript functions that perform asynchronous operations are typically expected to return a Promise. To convert a suspending function to a JavaScript async function, we can use promise on a scope. This will start a new coroutine that will run asynchronously, and return a Promise that resolves with the result of the coroutine.
fun fetchUserData(userId: String): Promise<JsUser> = scope.promise {
val user = getUserData(userId) // This is a suspending function
user.toJsUser() // Convert to JavaScript type
}
The scope is typically provided by a dependency injection framework, but it can be something as simple as CoroutineScope(SupervisorJob()). Such promises will run on Dispatchers.Default, which in JavaScript uses one thread to run all coroutines. This is similar to how JavaScript works, as it uses a single thread to run all async functions.
If you need to convert suspending functions into callback-based API, I explained that in a separate article. In short, you can just start an asynchronous coroutine using launch on a scope, and call the callback with the result of the suspending function. This is similar to how you would do it in JavaScript with async functions.
fun fetchUserDataWithCallback(
userId: String,
onSuccess: (JsUser) -> Unit,
onError: (Throwable) -> Unit
): Job = scope.launch {
try {
val user = getUserData(userId) // This is a suspending function
onSuccess(user.toJsUser()) // Convert to JavaScript type
} catch (e: Throwable) {
onError(e)
}
}
Converting Flow into JavaScript callback functions
We typically start and consume flow values using collect method. However, it is a suspending function, and cannot be called from JavaScript directly. There are a couple of ways how we deal with that. One is to define a wrapper function, that can be used in JavaScript to consume our flow. This function can start a coroutine that collects values from the flow and calls a callback with each value.
In JavaScript/TypeScript, we can generally meet four types of functions. The first one is a regular function that does not expect any callback or return any promise. Such functions can be called from Kotlin Coroutines without any problems, as they do not block the thread and do not require any special handling.
fun updateTitle(title: String) {
// Regular JavaScript functions can be called directly from both regular and suspending functions
document.findElementById("title")?.textContent = title
}
The second case is a function that returns a promise. Those are typically implemented as async function in JavaScript. Such functions can be handled like in JavaScript, using then and catch methods. However, in Kotlin Coroutines we can also use await extension function to turn them into suspending functions. This allows us to call them from suspending functions without blocking the thread.
@JsModule("./api.js")
external fun fetchUserData(userId: String): Promise<JsUser>
@JsModule("./api.js")
external fun saveUserPreferences(userId: String, preferences: JsPreferences): Promise<JsSaveResult>
// Calling a JavaScript async function from a regular function
fun savePreferences(userId: String, preferences: JsPreferences) {
saveUserPreferences(userId, preferences)
.then { result ->
console.log("Preferences saved successfully: $result")
}
.catch { error ->
console.error("Failed to save preferences: $error")
}
}
// Calling a JavaScript async function from a suspending function
suspend fun getUserData(userId: String): User {
return fetchUserData(userId).await().toUser()
}
The third case is when a function expects a callback. I dedicated a separate article to explain how to turn callback functions into suspending functions or flows. In short, when we expect a callback to be called once, we should turn it into a suspending function using suspendCancellableCoroutine. When we expect a callback to be called multiple times, we should turn it into a flow using callbackFlow.
@JsModule("./api.js")
external fun fetchUserDataWithCallback(
userId: String,
onSuccess: (JsUser) -> Unit,
onError: (Throwable) -> Unit
)
suspend fun getUserDataWithCallback(userId: String): User {
return suspendCancellableCoroutine { cont ->
fetchUserDataWithCallback(
userId,
onSuccess = { user -> cont.resume(user.toUser()) },
onError = { error -> cont.resumeWithException(error) }
)
}
}
@JsModule("./api.js")
external fun fetchUserUpdatesWithCallback(
onUpdate: (JsUserUpdate) -> Unit,
onError: (Throwable) -> Unit,
onComplete: () -> Unit,
)
fun userUpdatesFlow(): Flow<UserUpdate> = callbackFlow {
fetchUserUpdatesWithCallback(
onUpdate = { update ->
trySend(update.toUserUpdate())
},
onError = { error ->
cancel(CancellationException("Error fetching user updates", error))
},
onComplete = {
channel.close()
}
)
awaitClose { /* Cleanup if needed */ }
}.buffer(Channel.UNLIMITED)
Conclusion
Kotlin Coroutines can be used with JavaScript, allowing us to write cancellable asynchronous code for many platforms, including web applications. We can convert suspending functions to JavaScript async functions using promise, and we can convert callback-based APIs to suspending functions or flows using suspendCancellableCoroutine and callbackFlow. We can also convert JavaScript promises to suspending functions using await, and we can call regular JavaScript functions directly from Kotlin Coroutines. This allows us to write Kotlin code that can be used in JavaScript/TypeScript projects, while still benefiting from the power of Kotlin Coroutines.
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.