Coroutines Basics

Kotlin Intermediate — Coroutines Basics
Kotlin Intermediate
Course 2 · Chapter 1 · Coroutines Basics

🌀 Coroutines Basics

Coroutines are Kotlin's approach to asynchronous programming — lightweight, suspendable computations that let you write async code that reads like straight-line, sequential code, instead of nested callbacks. This chapter covers suspend functions, CoroutineScope, the launch and async builders, and Dispatchers — the vocabulary every later chapter in this course builds on.

📦 Setup: kotlinx.coroutines

Unlike val/var or null safety, coroutines aren't built into the core language syntax alone — launch, async, and Dispatchers come from the kotlinx.coroutines library (the suspend keyword itself is a language feature; the library provides the tools that actually run suspend functions). In a real project this is a Gradle dependency:

// build.gradle.kts dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") }

It ships pre-added in Android Studio projects and most Kotlin server templates, so this is usually a one-time setup step rather than something to think about chapter to chapter.

⏸️ suspend Functions

A function marked suspend can be paused and resumed without blocking the thread it's running on:

suspend fun fetchUserName(): String { delay(1000) // suspends for 1 second — does NOT block the thread return "Philip" }

delay() is the coroutine world's non-blocking equivalent of Thread.sleep() — it pauses the coroutine, freeing the underlying thread to do other work in the meantime, then resumes exactly where it left off once the delay is up. A suspend function can only be called from inside another suspend function, or from inside a coroutine — the compiler enforces this, so there's no way to accidentally call a slow suspending operation from ordinary, non-suspending code.

⚠ suspend Doesn't Mean "Runs on a Background Thread"

Marking a function suspend only means it can pause without blocking — it says nothing about which thread it runs on. That's a separate decision, controlled by Dispatchers (covered later in this chapter). A common beginner assumption is that suspend alone moves work off the main thread; it doesn't.

Sequential, Not Callback-Nested

Two suspend calls in a row (val a = fetchA(); val b = fetchB()) read top-to-bottom, exactly like synchronous code — no .then() chains, no nested callbacks, even though both calls are genuinely asynchronous under the hood.

vs JavaScript async/await

This will feel immediately familiar coming from JavaScript's async/awaitsuspend fun plays the role of an async function, and calling a suspend function reads like awaiting a promise, without a separate keyword at the call site.

🌐 CoroutineScope

A coroutine can't just be launched into the void — every coroutine belongs to a CoroutineScope, which defines its lifetime and ties it to a specific place in the code that "owns" it:

fun main() = runBlocking { // "this" here is a CoroutineScope, provided by runBlocking launch { delay(500) println("Coroutine finished") } println("main() continues immediately") }

runBlocking is a bridge between ordinary blocking code (like main(), or a test function) and the coroutine world — it blocks the current thread until every coroutine launched inside it completes. It's mainly a tool for main() functions and tests; real applications almost never use runBlocking elsewhere, since blocking a thread is exactly what coroutines exist to avoid.

⚠ Structured Concurrency

Every coroutine launched inside a scope is tied to that scope's lifetime — if the scope is cancelled, every coroutine inside it is cancelled too, automatically. This is called structured concurrency, and it's the core safety guarantee coroutines provide: there's no such thing as an orphaned coroutine silently running forever with nothing tracking it, unlike a raw background thread started and forgotten about.

🚀 launch — Fire and Forget

launch starts a new coroutine that runs independently and doesn't return a usable result — it's for work you care about happening, not work you need a value back from:

fun main() = runBlocking { val job = launch { delay(1000) println("Task done") } println("Task launched") job.join() // suspend here until the launched coroutine finishes } // Output: // Task launched // Task done (printed ~1 second later)

launch returns a Job — a handle representing the running coroutine. job.join() suspends the current coroutine until that job completes (without blocking the thread), and job.cancel() stops it early. If nothing ever calls join(), launch is genuinely "fire and forget" — the calling code moves on immediately while the coroutine runs in the background.

🔁 async / await — Getting a Result Back

async is launch's counterpart for when you do need a value back — it returns a Deferred<T>, which is Kotlin's name for what other languages call a Future or Promise:

suspend fun fetchScore(): Int { delay(500) return 42 } fun main() = runBlocking { val deferred = async { fetchScore() } println("Waiting...") val result = deferred.await() // suspends until the result is ready println("Score: $result") }

.await() is the direct Kotlin equivalent of JavaScript's await. The real payoff of async is running independent tasks concurrently, then collecting both results:

fun main() = runBlocking { val deferred1 = async { fetchScore() } // starts immediately, runs concurrently val deferred2 = async { fetchScore() } // starts immediately too, doesn't wait for deferred1 val total = deferred1.await() + deferred2.await() println("Total: $total") // takes ~500ms total, not ~1000ms — both ran at the same time }

launch vs async

launchasync
ReturnsJobDeferred<T>
Result value?NoYes, via await()
Use case"Do this" — no result needed"Compute this" — need the value back
Unhandled exceptionCrashes immediatelyHeld until await() is called

🧵 Dispatchers — Choosing Where Code Runs

A Dispatcher decides which thread (or thread pool) a coroutine actually runs on:

launch(Dispatchers.Default) { // CPU-intensive work — sorting, parsing, calculations } launch(Dispatchers.IO) { // Blocking I/O — network calls, file/database reads } launch(Dispatchers.Main) { // UI updates — Android's main thread, or similar UI-toolkit thread }

Dispatchers.Default

A thread pool sized to the number of CPU cores, meant for CPU-bound work that genuinely keeps a processor busy — computation, not waiting.

Dispatchers.IO

A larger, elastic thread pool meant for blocking I/O calls that spend most of their time waiting, not computing — network requests, file access, database queries.

Dispatchers.Main

Only available with a UI framework on the classpath (e.g. Android). Confined to the single UI thread — required for touching UI elements, and where a coroutine typically starts and ends even if it dispatches work elsewhere in between.

withContext(...)

Switches the dispatcher for a block of code within an already-running coroutine — e.g. do a network call on Dispatchers.IO, then withContext(Dispatchers.Main) to update the UI with the result.

Coroutines vs Java Threads vs JavaScript's Event Loop

Java ThreadsJavaScriptKotlin Coroutines
Unit of concurrencyOS thread (heavyweight, ~1MB stack)Single thread + event loopLightweight, thousands run per thread
Blocking a "slot"?Yes — a blocked thread is a wasted threadN/A — no blocking, only one threadNo — suspend frees the thread while waiting
True parallelism?Yes, across coresNo, single-threadedYes, via Dispatchers.Default/IO thread pools
CancellationCooperative, awkward (interrupt flags)No built-in cancellation for async/awaitStructured, built-in (Job.cancel())

Coroutines get the best of both worlds compared to what's likely familiar already: the readable, sequential code style of JavaScript's async/await, combined with real multi-core parallelism when needed (via Dispatchers.Default/IO) — something JavaScript's single-threaded model can't offer at all.

💻 Coding Challenges

Challenge 1: A Basic Suspend Function with launch

Write a suspend function fun printAfterDelay(message: String, delayMs: Long) that delays by delayMs then prints message. In main() (using runBlocking), use launch to start it with a 1000ms delay, print "Launched" immediately after, then use the returned Job's join() so the program waits for it to finish before exiting.

Goal: Practice writing a suspend function and launching it with launch + join().

→ Solution

Challenge 2: Concurrent async Calls

Write two suspend functions, fetchTemperature() and fetchHumidity(), each delaying 500ms and returning an Int. In main(), use async to start both concurrently, then await() both results and print them together. Add a comment noting roughly how long the whole thing should take.

Goal: Practice concurrent async calls and see they run in parallel, not sequentially.

→ Solution

Challenge 3: Choosing a Dispatcher

Write a suspend function fun simulateNetworkCall(): String that runs on Dispatchers.IO (using withContext), delays 300ms, and returns a string. Call it from main() via runBlocking and print the result, along with a comment explaining why Dispatchers.IO is the appropriate choice here rather than Dispatchers.Default.

Goal: Practice withContext() and reason about which dispatcher fits a given kind of work.

→ Solution

💡 This Chapter Is the Foundation for the Rest of Course 2

suspend/launch/async/Dispatchers are the vocabulary every later Course 2 chapter assumes — Flow (next chapter) is built on top of suspend functions, and later chapters on generics, delegation, and DSLs all show up in real Kotlin code that's frequently coroutine-based. Getting comfortable with structured concurrency now pays off throughout the rest of the course.

🎯 What's Next

Next chapter: Coroutines Advanced — Flow, StateFlow, SharedFlow, structured concurrency in depth, and cancellation.