Brifing

Kotlin Coroutines Anti-Patterns in Production: 5 Subtle Traps

Common pitfalls leading to memory leaks and concurrency issues.

ProAndroidDev - Medium2 min

1Kotlin Coroutines and Asynchronous Programming

Kotlin Coroutines revolutionized asynchronous programming in Android by making asynchronous code read like sequential, synchronous logic. However, structured concurrency relies on strict rules between parent and child jobs, context propagation, and cooperative cancellation. When these rules are subtly violated, apps can suffer from memory leaks, unhandled crashes, or silent execution freezes.

2Swallowing CancellationException

In Kotlin Coroutines, cancellation is cooperative and driven by CancellationException. When a CoroutineScope or Job is cancelled, a CancellationException is thrown inside active coroutines to abort execution immediately. However, catching Exception or Throwable without rethrowing CancellationException swallows the cancellation signal, allowing the coroutine to continue running even if its parent Job or ViewModelScope has been destroyed.

3Parallel Execution Without supervisorScope

When firing multiple concurrent requests with async, developers often assume that an error in one request will be caught by a try-catch block wrapping await(). However, under standard structured concurrency (coroutineScope), if any child coroutine launched via async throws an exception, it immediately cancels its parent and all sibling coroutines—before await() is even reached, potentially causing the original exception to go uncaught.

4High-Frequency Context Switching

Using withContext(Dispatchers.IO) is essential for offloading blocking I/O work from the main thread. However, switching execution contexts incurs significant allocation and dispatch overhead. Invoking withContext inside a loop processing hundreds or thousands of items causes excessive context switches, thread queuing, and garbage collector pressure, negatively impacting performance.

5GlobalScope Leaks in Long-Lived Singletons

Singletons or application-scoped components often need to launch background jobs, such as synchronizing local databases with backend or logging analytics. Using GlobalScope or CoroutineScope(Dispatchers.IO) directly without a controlled lifecycle creates unmanaged jobs. If an operation hangs, gets stuck in an infinite loop, or throws an unhandled exception, it cannot be safely cancelled from outside, leading to potential memory leaks.

6Refactoring and Solutions

To avoid swallowing CancellationException, explicitly rethrow it or catch domain-specific exceptions instead of general Exception. Wrapping parallel async operations in a supervisorScope isolates failures, preventing one child job's error from propagating to siblings or parent jobs. Moving withContext outside loops ensures the entire batch runs within a single dispatcher context shift. Injecting a CoroutineScope tied to the Application lifecycle, backed by SupervisorJob(), provides controlled job management.

7Performance Implications

These anti-patterns can severely impact application performance. Memory leaks increase memory usage, potentially causing crashes. High-frequency context switching, thread queuing, and garbage collector pressure can extend application response times. Uncontrolled GlobalScope usage can escalate resource consumption, degrading system performance.

8Conclusion and Recommendations

While Kotlin Coroutines simplify asynchronous programming in Android, violating structured concurrency rules can lead to significant issues. Recognizing and refactoring these anti-patterns can enhance application performance and stability. Regular code audits and testing can help detect such problems early, ensuring smoother app operation.

Original article: ProAndroidDev - Medium — Kotlin Coroutines'da Üretimde Karşılaşılan 5 Anti-Pattern