Coroutines — key concepts, Job and Dispatchers in Kotlin

Author: IT Sectr Published: 2026-03-16 Reading time: 8 min

Coroutines are lightweight threads in Kotlin for asynchronous programming, available through the kotlinx.coroutines library. According to JetBrains Kotlin Documentation, 2026, Coroutines allow suspending function execution without blocking a thread, unlike traditional Threads. Coroutines run on a limited thread pool, making them a thousand times lighter than native threads. Kotlin Coroutines are fully integrated with Android Jetpack, Retrofit, Room and other popular libraries of the Android ecosystem.

Key Takeaways

  • Coroutines — lightweight Kotlin threads for non-blocking asynchronous code
  • suspend function — a function that can pause and resume without blocking a thread
  • Dispatcher determines the thread pool for coroutine execution
  • Job — a coroutine handle with cancellation support and state tracking
  • CoroutineScope manages the coroutine lifecycle and their cancellation upon completion

What are Kotlin Coroutines

Coroutines are an asynchronous programming mechanism in Kotlin, implemented in the kotlinx.coroutines library. Unlike operating system threads, coroutines are not tied to a specific thread: they can suspend on one thread and resume on another. A single thread can execute thousands of coroutines, switching between them with minimal overhead.

Coroutines appeared in Kotlin 1.3 (2018) as an experimental feature and became stable in Kotlin 1.5 (2021). Coroutines solve the callback hell problem similar to async/await, but provide a richer API: channels (Channel), Flow, exception handling in the Job hierarchy, and direct integration with Android Lifecycle.

According to JetBrains (2025), each coroutine consumes about 100 bytes of memory versus 1+ MB for a native thread. This allows running millions of coroutines in a single application without the risk of OutOfMemoryError. It is the lightweight nature of coroutines that makes them the preferred tool for asynchronicity in Android.

How Coroutines Work Under the Hood

Each Kotlin coroutine is compiled into a state machine using Continuation Passing Style (CPS). The compiler adds a hidden Continuation parameter to each suspend function. The Continuation contains the resumption point and all local variables. When a coroutine suspends, the runtime saves the Continuation, and when it resumes, it restores it on any available thread from the Dispatcher pool.

Suspend Functions: Suspending and Resuming

suspend is a Kotlin keyword that marks a function as suspending. Such a function can only be called from another suspend function or from a coroutine. Inside a suspend function, you can call other suspend functions in any order, and each call point is a potential suspension point.

The mechanics are simple: when a suspend function calls another suspend function, it suspends at that point, freeing the thread. After the called function completes, the runtime continues execution from the saved location. This is called cooperative cancellation — no threads are blocked.

  • Suspension — the coroutine frees the thread without blocking it
  • Resumption — the coroutine continues from where it suspended
  • Thread — a coroutine can suspend on thread A and resume on thread B
  • Exceptions — handled via try/catch just like in synchronous code

Important: a suspend function is not asynchronous by default. The execution order remains sequential if launch or async are not used. suspend only allows the function to be paused without blocking the thread and to be part of the coroutine context. Continuation Passing Style is a compilation model where each suspend function receives a hidden Continuation callback, and the compiler generates a state machine to manage suspensions and resumptions.

CoroutineScope and Structured Concurrency

CoroutineScope is a context that defines the lifecycle of coroutines. All coroutines must be launched within a scope. When a scope is cancelled (e.g., when an Activity finishes), all its child coroutines are automatically cancelled. This prevents background task leaks. Android Jetpack provides ready-made scopes for each component: viewModelScope for ViewModel and lifecycleScope for Activity and Fragment, which are automatically cancelled when the corresponding component is destroyed.

Structured Concurrency is a principle that guarantees a coroutine will not complete until all its child coroutines have completed. The Job hierarchy forms a tree: a root coroutine creates a parent job, children create child jobs. Cancelling a parent job propagates to all children. Structured Concurrency is a fundamental difference between coroutines and threads.

ScopeWhere UsedCancellation
GlobalScopeDaemon tasks onlyNot cancelled automatically
viewModelScopeAndroid ViewModelOn ViewModel clearing
lifecycleScopeAndroid Activity/FragmentOn lifecycle destruction
coroutineScopeInside suspend functionOn parent job cancellation

SupervisorJob for Error Handling

A regular Job cancels all siblings when one child coroutine fails. SupervisorJob is an exception: a failure in one child coroutine does not affect the others. This is important when several independent tasks are executed in parallel, and one of them may crash without needing to cancel the others.

Dispatchers and Coroutine Builders

Dispatchers determine which threads execute coroutines. Dispatchers.Main — the main Android UI thread. Dispatchers.IO — a pool for blocking operations (network, disk). Dispatchers.Default — for CPU-intensive tasks. Dispatchers.Unconfined — starts in the current thread but does not guarantee to stay on it. Choosing the right Dispatcher is critical for performance: an IO task on Default will block the compute pool, while a CPU task on IO will create unnecessary threads.

withContext — a function for switching Dispatchers inside a coroutine. For example, a suspend function parsing JSON can switch to Dispatchers.Default for computation and return to Dispatchers.Main for UI updates. withContext is the most commonly used builder in Android development.

Three Main Coroutine Builders

launch — launches a coroutine, returns a Job, does not return a result (fire-and-forget). async — launches a coroutine, returns a Deferred from which the result can be obtained via await. runBlocking — blocks the current thread to execute a coroutine (only for tests and main functions). Choosing a builder depends on the scenario: launch is suitable for events and updates, async for tasks with a result, runBlocking only for tests or entry points.

Code Examples with Coroutines in Kotlin

Let us consider three practical scenarios: a basic coroutine with launch, a parallel call with async, and error handling with SupervisorJob.

Launching a Coroutine with launch

viewModelScope.launch launches a coroutine in the ViewModel context. When the ViewModel is cleared, the coroutine is automatically cancelled.

kotlin
class ProfileViewModel : ViewModel() {
    fun loadUser() {
        viewModelScope.launch(Dispatchers.IO) {
            val user = api.fetchUser()
            withContext(Dispatchers.Main) {
                showUser(user)
            }
        }
    }
}

Parallel Requests with async

coroutineScope with async launches three requests in parallel. Results are collected via .await(). If any request fails, all are cancelled.

kotlin
suspend fun loadDashboard(): Dashboard = coroutineScope {
    val user = async { api.fetchUser() }
    val posts = async { api.fetchPosts() }
    val stats = async { api.fetchStats() }
    Dashboard(user.await(), posts.await(), stats.await())
}

Error Handling with SupervisorJob

SupervisorJob allows each coroutine to complete independently. An error in one request does not cancel the others.

kotlin
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
scope.launch {
    try { api.fetchUsers() } catch (e: Exception) { log(e) }
}
scope.launch {
    try { api.fetchPosts() } catch (e: Exception) { log(e) }
}

Coroutines vs Threads: Comparison and Scenarios

Threads are an operating system primitive. Each thread has its own stack (~1 MB) and requires a system call for creation and switching. Coroutines are a language primitive, not tied to the OS. They use Continuation to save state and switch at the runtime level without system calls.

  • Memory — thread ~1 MB, coroutine ~100 bytes. A 10,000x difference
  • Creation — thread ~1 µs syscall, coroutine ~0.01 µs at JVM level
  • Switching — thread ~0.1 µs (syscall), coroutine ~0.001 µs (continuation)
  • Maximum — thousands of threads vs millions of coroutines per device
  • Cancellation — a thread cannot be cancelled externally (deprecated Thread.stop), a coroutine can via Job.cancel()

According to Google (2025), using coroutines instead of threads reduces memory consumption for background tasks in Android applications by 90–95%. All modern Android libraries (Retrofit, Room, WorkManager) have built-in coroutine support through suspend functions. Ktor (JetBrains HTTP client framework) is also built entirely on coroutines, providing suspend functions for each request without a callback API. Room supports coroutines through suspend functions in DAO, allowing database queries without blocking the main thread.

When to Use Threads Instead of Coroutines

Threads remain necessary for native code through JNI, long-running CPU-intensive blocking calls (video rendering, simulations), and when integrating with C libraries. For everything else — coroutines.

Frequently Asked Questions

How is a coroutine different from a thread?

Coroutine is a suspendable unit of work that executes on an existing thread. A thread is a system resource with its own stack. Coroutines are thousands of times lighter than threads and do not block resources when suspended.

What is Dispatchers.IO and how is it different from Default?

Dispatchers.IO is designed for blocking I/O operations (network, files) and can create new threads when necessary. Dispatchers.Default has a fixed-size pool (number of CPU cores) for CPU-intensive computations.

How do I cancel a running coroutine?

Job.cancel() cancels the coroutine and all its children. To check for cancellation inside a coroutine, use ensureActive() — it throws a CancellationException if the coroutine is cancelled.

Can coroutines be used with RxJava?

Yes — through the kotlinx-coroutines-rx3 library. It provides awaitSingle, awaitFirst and other functions to convert Observable/Single to suspend functions and back via flowable.

What is Flow in coroutines?

Flow is a cold asynchronous data stream, the coroutine equivalent of RxJava Observable. Flow emits values sequentially and completes with an exception or success. It supports map, filter, catch and other operators.

Summary

  • Coroutines — Kotlin lightweight threads with non-blocking suspension via Continuation Passing Style
  • suspend — the keyword for marking suspending functions
  • Dispatchers manage the thread pool: Main, IO, Default respectively
  • CoroutineScope ties the coroutine lifecycle to a component (Activity, ViewModel)
  • launch starts a coroutine without a result, async/await — with a result
  • Structured Concurrency guarantees hierarchical cancellation of child coroutines
  • Coroutines vs threads — coroutines are 10,000 times lighter and are the standard for Android

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also