article banner

Kotlin Coroutines and JavaScript

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.

fun <T> Flow<T>.subscribe( scope: CoroutineScope, onEach: ((T) -> Unit)? = null, onError: ((Throwable) -> Unit)? = null, onCompletion: (() -> Unit)? = null, ): Job { return this .let { flow -> onEach?.let { flow.onEach { onEach(it) } } ?: flow } .let { flow -> onCompletion?.let { flow.onCompletion { onCompletion() } } ?: flow } .let { flow -> onError?.let { flow.catch { onError(it) } } ?: flow } .launchIn(scope) }

For more convenience, we can instead define a wrapper class for a flow, that can be used in JavaScript directly to subscribe to the flow.

class FlowCallback<T>( private val flow: Flow<T>, private val scope: CoroutineScope, ) { fun subscribe( scope: CoroutineScope, onEach: ((T) -> Unit)? = null, onError: ((Throwable) -> Unit)? = null, onCompletion: (() -> Unit)? = null, ): Job { return flow .let { flow -> onEach?.let { flow.onEach { onEach(it) } } ?: flow } .let { flow -> onCompletion?.let { flow.onCompletion { onCompletion() } } ?: flow } .let { flow -> onError?.let { flow.catch { onError(it) } } ?: flow } .launchIn(scope) } }

Here is a view model wrapper:

class NewsViewModel( val getNews: GetNewsUseCase, ): BaseViewModel() { private val _state = MutableStateFlow<NewsState>(NewsState.Loading) val state: StateFlow<NewsState> = _state.asStateFlow() init { viewModelScope.launch { try { val news = getNews() _state.value = NewsState.Success(news) } catch (e: Throwable) { _state.value = NewsState.Error(e) } } } } class NewsViewModelJs( viewModel: NewsViewModel, ): JsViewModel(viewModel) { val state: FlowCallback<NewsState> = FlowCallback(viewModel.state, viewModel.viewModelScope) }

This is how FlowCallback can be observed in React/TypeScript:

function useStateFlow<T>(
    flow: FlowCallback<T>,
): T | undefined {
    const [state, setState] = useState<T | undefined>(undefined);

    useEffect(() => {
        const job = flow.subscribe(scope, (value) => setState(value));
        return () => job.cancel();
    }, [flow]);

    return state;
}

function NewsComponent({ viewModel }: { viewModel: NewsViewModelJs }) {
    const newsState = useStateFlow(viewModel.state);

    if (newsState === undefined) {
        return <div>Loading...</div>;
    }

    if (newsState instanceof NewsState.Error) {
        return <div>Error: {newsState.error.message}</div>;
    }

    return (
        <ul>
            {newsState.news.map((item) => (
                <li key={item.id}>{item.title}</li>
            ))}
        </ul>
    );
}

Calling JavaScript from Kotlin Coroutines

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.