Coroutines Intro

Reviewed & published by Brayan K

Kotlin is a modern, concise language whose coroutines let you write asynchronous, non-blocking code that reads like ordinary sequential code — no callback pyramids required.

Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end of this lesson you'll write suspend functions, launch coroutines, and run work concurrently with async/await.

What You'll Learn in This Lesson

1️⃣ suspend Functions and runBlocking

A suspend function can pause at suspension points (like delay) without blocking its thread. You can only call one from another suspend function or a coroutine builder. runBlocking bridges ordinary code into the coroutine world — common in main and tests.

import kotlinx.coroutines.*

// 'suspend' marks a function that can PAUSE without blocking a thread.
suspend fun fetchUser(): String {
    delay(1000)                  // suspends for 1s (non-blocking, not Thread.sleep)
    return "Ada"
}

// runBlocking bridges normal code and the coroutine world (used in main/tests).
fun main() = runBlocking {
    println("Fetching...")
    val user = fetchUser()       // suspends here, frees the thread to do other work
    println("Got user: $user")
    println("Done")
}

The code reads top-to-bottom like normal sequential logic, yet the delay doesn't freeze the thread — that's the magic of coroutines.

2️⃣ launch, async, and Concurrency

launch starts a fire-and-forget coroutine and returns a Job. async starts one that produces a result as a Deferred, which you read with await(). Two async calls run concurrently.

import kotlinx.coroutines.*

suspend fun loadProfile(): String { delay(1000); return "profile" }
suspend fun loadSettings(): String { delay(1000); return "settings" }

fun main() = runBlocking {
    // launch starts a coroutine that runs concurrently (fire-and-forget).
    val job = launch {
        delay(500)
        println("Background task finished")
    }

    // async starts a coroutine that returns a Deferred result.
    // Two async calls run AT THE SAME TIME, so total wait ~1s, not 2s.
    val profile = async { loadProfile() }
    val settings = async { loadSettings() }

    // await() suspends until each result is ready:
    println("Loaded: ${profile.await()} + ${settings.await()}")

    job.join()                   // wait for the launched job too
    println("All done")
}

Both loads each take a second, but because they ran concurrently the total wait is about one second, not two. That's how coroutines turn sequential waits into parallel ones.

Your turn. Replace the TODO, then run and compare.

// 🎯 YOUR TURN — replace each TODO then run main()
import kotlinx.coroutines.*

// 1) Mark this function so it can call delay().
TODO fun greetAfterDelay(): String {   // 👉 replace TODO with 'suspend'
    delay(500)
    return "Hello after delay"
}

fun main() = runBlocking {
    println("Start")
    println(greetAfterDelay())          // suspends, then returns
    println("End")

    // ✅ Expected:
    //    Start
    //    Hello after delay
    //    End
}

Mini-Challenge: Concurrent Loads

Run two suspend functions concurrently with async and combine their results.

// 🎯 MINI-CHALLENGE: load two things concurrently.
// Save as Concurrent.kt and run main().
//
// 1. Write suspend fun price(): Int { delay(300); return 100 }.
// 2. Write suspend fun tax(): Int { delay(300); return 20 }.
// 3. In runBlocking, start BOTH with async, then await() each and
//    print "Total: <price + tax>".
//    (Because they run concurrently, this takes ~300ms, not 600ms.)
//
// ✅ Example output:
//    Total: 120

import kotlinx.coroutines.*

fun main() = runBlocking {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Coroutines

ConceptKotlin Syntax
Suspend functionsuspend fun load()
Non-blocking waitdelay(1000)
Bridge into coroutinesrunBlocking { ... }
Fire-and-forgetlaunch { ... }
Concurrent resultasync { ... }
Get the resultdeferred.await()

🎉 Lesson Complete!

Practice quiz

What does the 'suspend' keyword mark a function as?

  • A function that can pause at suspension points without blocking its thread
  • A function that always runs on a new thread
  • A function that cannot be cancelled
  • A function that blocks the main thread

Answer: A function that can pause at suspension points without blocking its thread. suspend marks a function that may pause and resume later without blocking the underlying thread.

Where can a suspend function be called from?

  • Any ordinary function
  • Only the main function
  • Another suspend function or a coroutine builder like launch/async/runBlocking
  • Only from inside a class

Answer: Another suspend function or a coroutine builder like launch/async/runBlocking. Suspend functions can only be called from another suspend function or a coroutine builder.

How does delay() differ from Thread.sleep()?

  • delay() is slower than Thread.sleep()
  • They are identical
  • delay() blocks two threads at once
  • delay() suspends the coroutine and frees the thread; Thread.sleep blocks the thread

Answer: delay() suspends the coroutine and frees the thread; Thread.sleep blocks the thread. delay suspends only the current coroutine, releasing the thread; Thread.sleep blocks the whole thread.

What does the launch coroutine builder return?

  • A Job
  • A Deferred
  • A String
  • Nothing at all

Answer: A Job. launch starts fire-and-forget work and returns a Job you can join or cancel.

What does async return?

  • A Job
  • A Deferred that you read with await()
  • A plain value immediately
  • A Thread

Answer: A Deferred that you read with await(). async returns a Deferred<T>; call await() to get the result.

What is runBlocking typically used for?

  • Cancelling all coroutines
  • Replacing the JVM thread pool
  • Bridging ordinary code into the coroutine world, e.g. in main and tests
  • Creating new threads only

Answer: Bridging ordinary code into the coroutine world, e.g. in main and tests. runBlocking bridges normal blocking code and the coroutine world.

How do you get the result out of a Deferred?

  • By calling .join()
  • By calling .await()
  • By calling .close()
  • By reading .value directly

Answer: By calling .await(). await() suspends until the Deferred's result is ready and returns it.

Starting two async blocks and then awaiting both lets the work run...

  • Sequentially, one after the other
  • On the UI thread only
  • Never, it deadlocks
  • Concurrently, so total wait is about one delay not two

Answer: Concurrently, so total wait is about one delay not two. Both async coroutines run concurrently, so two 1s tasks finish in about 1s total.

What does structured concurrency guarantee about child coroutines in a scope?

  • They are cancelled together with the scope
  • They run on separate processes
  • They never get cancelled
  • They ignore their parent scope

Answer: They are cancelled together with the scope. Children launched in a scope are cancelled together, preventing leaks by design.

Which Dispatcher is intended for network and disk work?

  • Dispatchers.Main
  • Dispatchers.Default
  • Dispatchers.IO
  • Dispatchers.Unconfined only

Answer: Dispatchers.IO. Dispatchers.IO is for blocking I/O like network and disk; Default is for CPU work.

Continue this course

Frequently asked questions

What is a coroutine, in plain terms?

A coroutine is a lightweight unit of work that can suspend (pause) and later resume without blocking the underlying thread. Think of it as a function you can pause partway through — say, while waiting for a network response — and pick up again later, all while the thread is free to run other coroutines. Because they're cheap, a single thread can host thousands of coroutines, which is why they're great for highly concurrent apps and Android UIs that must stay responsive.

What does the 'suspend' keyword mean?

suspend marks a function that may pause execution at certain points (its suspension points, like delay or a network call) and resume later. A suspend function can only be called from another suspend function or from inside a coroutine builder such as launch, async, or runBlocking — never from ordinary code directly. This rule makes the asynchronous boundaries visible in your code, so you always know which functions might pause.

How is delay different from Thread.sleep?

Thread.sleep blocks the whole thread — it sits there doing nothing, wasting that thread. delay suspends only the current coroutine and releases the thread so other coroutines can run during the wait. That's the essence of non-blocking concurrency: instead of freezing a thread, the coroutine steps aside and is resumed when the time is up. Use delay inside coroutines; never Thread.sleep.

When do I use launch versus async?

Use launch for fire-and-forget work where you don't need a return value — it returns a Job you can join or cancel. Use async when you want a result back: it returns a Deferred<T>, and you call await() to get the value (suspending until it's ready). A common pattern is to start several async calls so they run concurrently, then await each — turning sequential waits into parallel ones, which is exactly what the concurrent example demonstrates.