Skip to content
Shushant TiwariMobile engineer & maker
← All stories

Mastering NonCancellable in Kotlin Coroutines: Ensuring Safe Cleanup and Transactions

When cancellation should not interrupt cleanup, and how Kotlin’s NonCancellable context can help protect critical work.

Shushant TiwariMobile engineer. Building tools that help us see.
Contents

Kotlin coroutines provide structured concurrency, allowing developers to manage background tasks efficiently. However, when a coroutine is canceled, all its child coroutines and their associated operations are also canceled. This can lead to issues in scenarios where certain operations, such as logging, resource cleanup, or committing a database transaction, must be completed regardless of cancellation.To address this, Kotlin provides NonCancellable, a special coroutine context that allows critical operations to execute even if the coroutine is canceled. This article explores NonCancellable, its use cases, and how to apply it effectively in mobile app development.What is NonCancellable in Kotlin Coroutines?NonCancellable is a predefined object in Kotlin coroutines that prevents cancellation from affecting certain operations. It is typically used with withContext() to override the default cancellation behavior.

Syntax of NonCancellable

import kotlinx.coroutines.*
suspend fun performCriticalTask() {
    withContext(NonCancellable) {
        // Code that must execute even if the coroutine is cancelled
    }
}

Use Cases for NonCancellable

  1. Ensuring Cleanup in a try-finally Block

When a coroutine is canceled, the finally block still executes. However, if it contains a suspending function, that function might get canceled before completing. Using NonCancellable ensures that cleanup operations run completely.ProblemConsider a scenario where a coroutine performs a database operation and needs to release resources upon cancellation.

suspend fun performDatabaseOperation() {
    try {
        // Simulating a database operation
        delay(5000)
        println("Database operation completed")
    } finally {
        // Cleanup resources
        println("Releasing resources...") // This might get canceled
    }
}

If the coroutine running performDatabaseOperation() gets canceled before completion, the cleanup operation might be skipped.

Solution: Use NonCancellable

suspend fun performDatabaseOperation() {
    try {
        delay(5000)
        println("Database operation completed")
    } finally {
        withContext(NonCancellable) {
            println("Releasing resources safely")
        }
    }
}

Here, withContext(NonCancellable) ensures that resource cleanup executes even if the coroutine is canceled.

2. Committing a Database Transaction

In real-world mobile applications, database transactions must be committed or rolled back properly, even if the coroutine handling them is canceled.ProblemIf a transaction commit is a suspending function and the coroutine is canceled, the transaction might be left in an inconsistent state.

suspend fun saveData() {
    try {
        // Start transaction
        delay(2000)
        println("Data processing started")
        delay(3000) // Simulating transaction execution
    } finally {
        println("Committing transaction") // Might be skipped
    }
}

Solution: Use NonCancellable to Ensure Transaction Commit

suspend fun saveData() {
    try {
        delay(2000)
        println("Data processing started")
        delay(3000)
    } finally {
        withContext(NonCancellable) {
            println("Committing transaction safely")
        }
    }
}

Using NonCancellable ensures that the transaction is committed, preventing potential data inconsistencies.

3. Logging Critical Events

Logging is essential in mobile applications for debugging and tracking errors. If a coroutine is canceled before logging completes, important log entries might be lost.

suspend fun processRequest() {
    try {
        println("Processing request...")
        delay(3000)
    } finally {
        println("Logging cancellation event") // Might get canceled
    }
}

Solution: Use NonCancellable for Logging

suspend fun processRequest() {
    try {
        println("Processing request...")
        delay(3000)
    } finally {
        withContext(NonCancellable) {
            println("Logging cancellation event safely")
        }
    }
}

Ensuring logs are written even after cancellation helps in debugging and auditing.

**Best Practices for Using NonCancellable
**• Use NonCancellable only for critical operations like resource cleanup, logging, or transaction commits.
• Avoid long-running tasks inside NonCancellable, as they will continue running even after cancellation.
• Handle exceptions properly within NonCancellable blocks to prevent crashes.

suspend fun safeCleanup() {
    try {
        withContext(NonCancellable) {
            println("Performing critical cleanup")
        }
    } catch (e: Exception) {
        println("Error during cleanup: ${e.message}")
    }
}

Key Takeaways

  • NonCancellable ensures that critical operations (like cleanup, logging, or committing a transaction) execute even if the coroutine is canceled.
  • It should be used only for essential tasks that must not be interrupted.
  • Avoid running long or blocking operations inside NonCancellable to prevent resource leaks.
  • Always handle exceptions properly within NonCancellable to ensure stability.

By applying these best practices, developers can ensure that essential operations are executed safely without disrupting app performance or user experience.

Originally published on Medium on 19 Feb 2025. Republished here by the author.

Examples and opinions reflect the original publication date.

Keep the conversation going.

Have a thought to add?Join the conversation on Medium.