Bad Nesting in Kotlin Coroutines: The Bug That’s not a Bug (Until it is)

“Wait… why is this coroutine still running after my function ended?”
– Me, a few months ago, squinting at logs and questioning reality

What is “Bad Nesting”?

We all love Kotlin coroutines because they’re clean, powerful, and great for …


This content originally appeared on DEV Community and was authored by Abizer

“Wait… why is this coroutine still running after my function ended?”

– Me, a few months ago, squinting at logs and questioning reality

What is “Bad Nesting”?

We all love Kotlin coroutines because they’re clean, powerful, and great for writing async code that almost feels synchronous. But sometimes, coroutines betray us in the most subtle ways.

One such betrayal: bad nesting.

It happens when you put a coroutine builder like launch or async inside a withContext block, expecting it to behave like structured concurrency.

Spoiler alert: it doesn’t.

The Problem in a Nutshell

Bad nesting breaks structured concurrency and leads to:

  • Orphaned coroutines (they outlive the parent)
  • Logs that lie to you (“done” isn’t really done)
  • Concurrency bugs that make you question your sanity

Let’s See It in Action

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Before withContext")

    withContext(Dispatchers.IO) {
        launch {
            delay(1000)
            println("Inside launch")
        }
        println("withContext block done")
    }

    println("After withContext")
}

Output:

Before withContext  
withContext block done  
After withContext  
Inside launch

Wait... the launch runs after the outer scope thinks everything’s done?
Yes. And here’s why.

What's Really Happening?

Let’s break it down:

  1. runBlocking starts your main coroutine.
  2. withContext(IO) suspends and shifts work to an IO thread.
  3. Inside that block, you call launch. This creates a new coroutine, not tracked by withContext.
  4. withContext runs the block, hits the last line (println) and… finishes.
  5. The program resumes, even though launch is still running.

That launch is now a zombie coroutine, alive and unsupervised.
Remember: Job of withContext() is to switch dispatchers (threads) without starting a new coroutine. It doesn't track and wait for any coroutines launched inside it before returning.

Let Me Paint You a Picture

Imagine you're a team lead. You tell your assistant:

“Go to the warehouse and make sure all boxes are stacked.”

The assistant walks in, but instead of doing the stacking, he calls someone else and immediately walks out.

“Boxes are stacked, boss!”

Meanwhile, the boxes are still lying around, unstacked.

That’s exactly what happens when you launch inside withContext. The block returns, but the actual task isn’t finished.

Why Is This Dangerous?

  • You might start reading shared data before it’s been updated.
  • Cleanup might run before a job is complete.
  • Background tasks might leak or throw unexpected errors.

You think everything is done, but some coroutine is silently working in the background. That's a recipe for race conditions and flaky bugs.

How to Fix It

You’ve got two clean options depending on what you want:

1. Just do the work inside withContext

withContext(Dispatchers.IO) {
    delay(1000)
    println("Done properly")
}

No launch. Just let withContext suspend until it’s done.

2. Use coroutineScope inside withContext if you need multiple launches

withContext(Dispatchers.IO) {
    coroutineScope {
        launch {
            delay(1000)
            println("Task 1 done")
        }
        launch {
            delay(500)
            println("Task 2 done")
        }
    }
}

Output:

Task 2 done  
Task 1 done  
After all tasks

coroutineScope ensures that withContext won’t finish until all its child coroutines have completed.

Bad Nesting in Real Life

1. Orphaned Coroutine Example

withContext(Dispatchers.IO) {
    launch {
        delay(1000)
        println("Still running after withContext ends 😵")
    }
    println("withContext done")
}
println("runBlocking done")

Output:

withContext done  
runBlocking done  
Still running after withContext ends 😵

2. Race Condition Example

withContext(Dispatchers.IO) {
    launch {
        delay(1000)
        println("Updating shared resources")
    }
    println("Assuming updates are done 🤡")
}
println("Reading shared resources 😬")

🧾 Output:

Assuming updates are done 🤡  
Reading shared resources 😬  
Updating shared resources

Yikes.

Final Thoughts

Bad nesting is sneaky because it looks innocent, but it quietly breaks everything structured concurrency stands for.

Next time you're inside a withContext, ask yourself:

“Am I doing the work, or am I delegating it?”

If it's the latter, make sure you're supervising the workers properly using coroutineScope. 😉

✍️ About the Author

Hey! I’m Abizer, an Android developer who’s into finding weird bugs that make for great blog posts.

If this helped you out, follow me here or connect on GitHub / LinkedIn.

Have you been bitten by bad coroutine nesting? Share your bug story in the comments!


This content originally appeared on DEV Community and was authored by Abizer


Print Share Comment Cite Upload Translate Updates
APA

Abizer | Sciencx (2025-07-30T08:16:51+00:00) Bad Nesting in Kotlin Coroutines: The Bug That’s not a Bug (Until it is). Retrieved from https://www.scien.cx/2025/07/30/bad-nesting-in-kotlin-coroutines-the-bug-thats-not-a-bug-until-it-is/

MLA
" » Bad Nesting in Kotlin Coroutines: The Bug That’s not a Bug (Until it is)." Abizer | Sciencx - Wednesday July 30, 2025, https://www.scien.cx/2025/07/30/bad-nesting-in-kotlin-coroutines-the-bug-thats-not-a-bug-until-it-is/
HARVARD
Abizer | Sciencx Wednesday July 30, 2025 » Bad Nesting in Kotlin Coroutines: The Bug That’s not a Bug (Until it is)., viewed ,<https://www.scien.cx/2025/07/30/bad-nesting-in-kotlin-coroutines-the-bug-thats-not-a-bug-until-it-is/>
VANCOUVER
Abizer | Sciencx - » Bad Nesting in Kotlin Coroutines: The Bug That’s not a Bug (Until it is). [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/07/30/bad-nesting-in-kotlin-coroutines-the-bug-thats-not-a-bug-until-it-is/
CHICAGO
" » Bad Nesting in Kotlin Coroutines: The Bug That’s not a Bug (Until it is)." Abizer | Sciencx - Accessed . https://www.scien.cx/2025/07/30/bad-nesting-in-kotlin-coroutines-the-bug-thats-not-a-bug-until-it-is/
IEEE
" » Bad Nesting in Kotlin Coroutines: The Bug That’s not a Bug (Until it is)." Abizer | Sciencx [Online]. Available: https://www.scien.cx/2025/07/30/bad-nesting-in-kotlin-coroutines-the-bug-thats-not-a-bug-until-it-is/. [Accessed: ]
rf:citation
» Bad Nesting in Kotlin Coroutines: The Bug That’s not a Bug (Until it is) | Abizer | Sciencx | https://www.scien.cx/2025/07/30/bad-nesting-in-kotlin-coroutines-the-bug-thats-not-a-bug-until-it-is/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.