5 Fatal Coroutine Mistakes Nobody Talks About in Android

Emmanuel Iyke
7 min readFeb 5, 2025

--

Introduction:

As Android developers, Kotlin Coroutines have become an essential part of our daily workflow. They make asynchronous programming and concurrency handling feel effortless — almost too effortless. But that simplicity can be deceptive. Many developers unknowingly fall into subtle yet dangerous pitfalls that, at first glance, seem like best practices.

Today, we’re pulling back the curtain to x-ray these hidden mistakes, explore their real-world consequences, and uncover better, more efficient alternatives. Buckle up — this might just save your next project from unexpected crashes, memory leaks, and performance nightmares!

1: Calling suspend Function Directly in Views

Calling a suspend function directly inside a ViewModel may seem harmless, but it can lead to serious lifecycle issues and main thread blocking problems if not handled properly. This is especially common in MVP (Model-View-Presenter) architecture, where developers try to fetch data and update UI state directly from a suspend function inside ViewModel.

Why is This a Problem?

  • Blocking the Main Thread: If a suspend function performs a long-running operation (e.g., network call) without switching to a background thread (e.g., Dispatchers.IO), it can block the main thread, causing lag or ANRs (Application Not Responding errors).
  • Lifecycle Issues: ViewModels survive configuration changes, but if coroutines are not properly scoped (e.g., using viewModelScope), they might continue executing even after the UI is destroyed, leading to memory leaks or wasted resources.
  • Lost Network Results: If the coroutine is tied to a ViewModel that gets cleared (e.g., when the activity/fragment is finished), the result might never reach the UI, as the coroutine will be canceled.

Example of the Wrong Approach

class MyViewModel(private val repository: MyRepository) : ViewModel() {

suspend fun fetchUserData() {
val user = repository.getUser() ❌
}
}

What’s Wrong Here?

  • The fetchUserData function is marked as suspend, but it doesn’t handle threading or lifecycle properly. It also doesn’t expose the result to the UI, making it useless for updating the UI state.

Correct Approach:

class MyViewModel(private val repository: MyRepository) : ViewModel() {

private val _user = MutableLiveData<User>()
val user: LiveData<User> get() = _user

fun fetchUserData() {
viewModelScope.launch {
try {
val result = repository.getUser() // ✅ Safe coroutine call
_user.postValue(result) // ✅ Updates UI on the Main thread
} catch (e: Exception) {
// Handle error properly
}
}
}
}

The fetchUserData function uses viewModelScope.launch to safely execute the coroutine within the ViewModel's lifecycle. The result is exposed to the UI using LiveData, ensuring that the UI is updated on the main thread.

2) Incorrect use of Global scope

Coroutines are a powerful tool in Kotlin, but misusing GlobalScope can cause major issues like memory leaks, unmanageable background tasks, and crashes. Many developers unknowingly use it incorrectly, leading to problems that are hard to debug, especially in Android development where lifecycle awareness is crucial."

Why is this a problem

using GlobalScope.launch in an Android application is problematic because:

  • It Cannot Be Canceled — Once started, a coroutine in GlobalScope runs independently of the app's lifecycle. If the user leaves the screen, the coroutine keeps running in the background, potentially wasting resources.
  • It Ignores Lifecycle Awareness — Unlike viewModelScope, GlobalScope doesn’t cancel coroutines when the ViewModel is cleared. This means data updates might be attempted on a destroyed UI, causing crashes and unexpected behaviors.
  • No Custom Dispatcher or Exception HandlingGlobalScope.launch always defaults to Dispatchers.Default, which may not be optimal for network requests (which should run on Dispatchers.IO). Additionally, unhandled exceptions in GlobalScope can crash the entire app, as there is no structured concurrency to handle them properly.

Example of Wrong approach

GloablScope.launch{
delay(400)
Log.d("Global scope", "Task Successful")
}

How to Fix It

To ensure proper lifecycle management, always use viewModelScope.launch instead of GlobalScope.launch.

viewmodelscope.launch{
delay(400)
Log.d("viewmodel", "Task Successful")
}

3) Fetching Data Sequentially Instead of Asynchronously

I still feel a bit frustrated about this mistake because I made it during my interview with Fairmoney, and it ended up costing me the job offer. One of the common pitfalls developers face with Kotlin coroutines is executing network calls sequentially when they could be run in parallel. This can lead to significantly longer execution times, causing unnecessary delays and affecting the user experience.

Why is This a Problem?

Fetching data sequentially (one after the other) is inefficient, especially when:

  • Each request is independent — If fetching function A doesn’t depend on fetchingfunction B, running them sequentially is wasteful and increases the total execution time unnecessarily.
  • Network calls are time-consuming – Running multiple 1-second calls one after the other scales linearly (5 calls = 5 seconds!).
  • User experience suffers – The app hangs longer than necessary, leading to frustration.

Example of the wrong approach

suspend fun mistakeGetCarNames(ids: List<Int>): List<String> {
val names = mutableListOf<String>()
for (id in ids) {
names.add(getCarNameById(id)) ❌ // Wrong: Calls run one after another, wasting time!
}
return names
}

What’s Wrong Here?

  • Each network call must finish before the next one starts.
  • Time wasted — If each request takes 1s and there are 5 requests, total time = 5s.
  • Unnecessary delays – The app becomes sluggish, blocking the UI longer than needed.

How to fix it

We can fetch all data concurrently using async {} inside coroutineScope {}.

Optimized Code Using async-awaitAll (Good Code)

suspend fun getCarNames(ids: List<Int>): List<String> {
return coroutineScope {
ids.map { id ->
async { getCarNameById(id) } // ✅ Runs all requests in parallel!
}.awaitAll() // ✅ Waits for all coroutines to complete and returns results
}
}
  • Runs all network calls concurrently — Instead of running them one by one, async allows them to start at the same time.
  • Reduces total execution time – If each call takes 1s and there are 5 calls, total time ≈ 1s instead of 5s.
  • Better user experience – Faster responses mean the app feels more responsive.

4) Catching CancellationException in Suspend Functions:

One of the biggest coroutine mistakes in Android development is catching CancellationException without properly rethrowing it back to the parent. This leads to unexpected behavior, where canceled coroutines continue running when they should stop immediately, potentially causing memory leaks and wasted resources.

Why is this a problem

When a coroutine is canceled (e.g., due to a lifecycle event like Activity destruction), it throws a CancellationException.

  • If you catch this exception and do not rethrow it, the parent coroutine won’t know it was canceled!
  • This can cause memory leaks, UI lag, and unnecessary background processing.

Example of the wrong approach

suspend fun mistakeRiskyTask() {
try {
delay(3_000L) // Simulating long-running operation
val error = 10 / 0 // ❌ This will throw an ArithmeticException
} catch (e: Exception) {
println("iyke: code failed") ❌ // Catching all exceptions, including CancellationException!
}
}

What’s Wrong Here?

  • Catches CancellationException along with other exceptions – If the coroutine was canceled, the parent coroutine won’t know.
  • Coroutine continues running after being canceled – This can lead to wasted CPU cycles and memory leaks.
  • Unexpected behavior – The app might appear unresponsive or laggy because coroutines aren’t stopping when they should.

How to fix it

suspend fun riskyTask() {
try {
delay(3_000L) // Simulating long-running operation
val error = 10 / 0 // ❌ This will throw an ArithmeticException
} catch (e: Exception) {
if (e is CancellationException) throw e // ✅ Rethrow CancellationException!
println("Emmanuel iyke: code not working") // ✅ Handle other exceptions properly
}
}
  • Always rethrow CancellationException inside a catch block so the parent coroutine is aware of it.
  • Only handle other exceptions (e.g., IOException, ArithmeticException) separately

5) Checking for coroutine cancellation

One common mistake when using coroutines in Kotlin is not checking if the coroutine is still active while executing long-running tasks

Why is This a Problem?

  • If a coroutine is canceled but your loop keeps running, the coroutine never actually stops, leading to:
  • Wasted CPU resources — The loop continues executing, consuming unnecessary processing power.
  • Memory leaks — The coroutine remains in memory even when it’s no longer needed.
  • UI lag or app crashes — If the coroutine is working on UI updates, this could cause unexpected UI behavior.

Example of the wrong approach

suspend fun mistakeDoSomething() {
val job = CoroutineScope(Dispatchers.Default).launch {
var random = Random.nextInt(100_000)
while (random != 50_000) { // ❌ No check for cancellation
println("Random: $random")
random = Random.nextInt(100_000)
}
}
println("Random: our job cancelled")
delay(500L)
job.cancel() // ❌ This won't immediately stop the loop
}

What's wrong here

  • Job is canceled, but the loop doesn’t stop immediately — The while loop keeps running because it doesn’t check if the coroutine is still active.
  • Leads to unnecessary CPU usage – The loop runs until it reaches 50_000, even though we canceled the job.
  • Unreliable behavior – You expect it to stop when canceled, but it keeps running until completion.

How to fix it

suspend fun doSomethingWithIsActive() {
val job = CoroutineScope(Dispatchers.Default).launch {
var random = Random.nextInt(100_000)
while (isActive && random != 50_000) { // ✅ Manually check if coroutine is active
println("Random: $random")
random = Random.nextInt(100_000)
}
}
println("Random: our job cancelled")
delay(500L)
job.cancel() // ✅ Coroutine will now stop properly
}
  • Instead of isActive, you can use ensureActive(), which is a built-in property of coroutines.
  • Use isActive inside loops – It throws a CancellationException if the coroutine is canceled, stopping it immediately.

Conclusion

Kotlin Coroutines are powerful tools for building scalable, responsive, and efficient Android applications. By understanding and avoiding common pitfalls — such as improper scope usage, unhandled exceptions, and ignoring coroutine cancellation — you can write more efficient, reliable, and maintainable asynchronous code.

🔹 Always choose the correct dispatcher.
🔹 Tie your coroutines to the appropriate lifecycle.
🔹 Handle exceptions thoughtfully.
🔹 Prioritize structured concurrency and lifecycle awareness.
🔹 Test your coroutine code thoroughly.

By following these best practices, you’ll harness the full potential of Kotlin Coroutines, ensuring robust app performance and a smooth user experience.

🙌 Thank You for Reading!
If you have questions or need help with Android or Kotlin, feel free to reach out. Let’s connect and grow together! 🚀

🔗 Follow me on:

👏 Don’t forget to clap and follow for more articles on Android Development, Kotlin, Jetpack Compose, and best practices!

--

--

Emmanuel Iyke
Emmanuel Iyke

Responses (2)