Popular Searches
Popular Course Categories
Popular Courses

Managing Coroutine Cancellation and Exceptions

Managing Coroutine Cancellation and Exceptions

Coroutines and Asynchronous Programming

Managing Coroutine Cancellation and Exceptions

Course: Android App Development

Topic: Managing Coroutine Cancellation and Exceptions

Course Link: Android App Development Course

Demo Link: Register for a Course Demo

Website: JustAcademy


1. Introduction

Kotlin Coroutines make asynchronous programming easier in Android applications. They allow developers to perform network requests, database operations, file processing, and other time-consuming tasks without unnecessarily blocking the main thread. However, real applications must also handle situations in which a task is no longer required or an operation fails unexpectedly.

Coroutine cancellation is the process of stopping coroutine work that is no longer needed. Exception handling is the process of managing failures that occur while coroutine code is executing.

For example, consider an Android application that retrieves product information from a server. If the user leaves the screen before the request completes, the application should avoid performing unnecessary screen-related work. If the server request fails because of a network problem, the application should show an appropriate error instead of leaving the screen in an incorrect state.

Understanding cancellation and exceptions helps developers build Android applications that are responsive, reliable, resource-efficient, and easier to maintain.

2. Learning Objectives

After completing this topic, you should be able to:

  • Understand coroutine cancellation and exception handling.
  • Explain the role of Job and Deferred.
  • Cancel coroutines manually and understand automatic cancellation.
  • Understand cooperative cancellation and cancellation checks.
  • Use try-catch-finally with suspending functions.
  • Handle CancellationException correctly.
  • Understand exception propagation in parent-child coroutine relationships.
  • Differentiate launch() and async() exception behavior.
  • Understand CoroutineExceptionHandler and supervision.
  • Apply cancellation and exception handling in Android ViewModels.
  • Identify common mistakes and follow coroutine best practices.

3. Understanding Coroutine Cancellation

Cancellation means requesting a coroutine to stop its work before it completes normally. Cancellation is useful when a user navigates away from a screen, enters a new search query, closes a component, or starts a newer operation that makes an earlier operation unnecessary.

In Kotlin, each coroutine has a job that represents its lifecycle. Coroutine builders such as launch() return a Job, while async() returns a Deferred, which is also a job.

Why cancellation is important

  • Resource management: Avoid unnecessary processing and waiting.
  • UI correctness: Reduce the risk of outdated operations updating a screen.
  • Performance: Stop obsolete calculations and requests when possible.
  • Lifecycle management: Connect background work to the lifecycle of its owner.
  • Application reliability: Keep related tasks under predictable lifecycle control.

Example scenario

A user searches for a product by typing “lap” and then quickly changes the query to “laptop”. The application may start two requests. Cancelling the obsolete request can reduce wasted work, although the networking library and server must also support or handle request cancellation appropriately.

4. Understanding Job and Deferred

A Job represents a coroutine's lifecycle and provides functions for cancellation and waiting for completion. A Deferred represents an asynchronous result and also supports job-related operations.

Feature Job Deferred
Purpose Represents coroutine work. Represents coroutine work that produces a result.
Created by Commonly returned by launch(). Returned by async().
Cancellation Supports cancel(). Supports cancel() because it implements Job.
Waiting Use join(). Use await() to obtain its result.
Example Performing a background notification task. Calculating a total that must be returned.

5. Cancelling a Coroutine Manually

You can cancel a coroutine by calling cancel() on its job. You can then call join() if you need to wait until the coroutine has finished its cancellation and cleanup.

Example

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        repeat(10) { index ->
            delay(300)
            println("Processing item: $index")
        }
    }

    delay(800)
    println("Requesting cancellation")

    job.cancel()
    job.join()

    println("Coroutine has completed")
}

Explanation

  1. runBlocking creates a coroutine scope for this command-line demonstration.
  2. launch() starts a child coroutine.
  3. The coroutine processes items with a delay between iterations.
  4. The parent calls cancel() after approximately 800 milliseconds.
  5. The child responds to cancellation at a cancellable suspension point.
  6. join() waits for the job to finish.

Important: runBlocking is used here only to demonstrate coroutine behavior. Do not use it on the Android main thread to perform application work because it blocks the calling thread.

6. Automatic Cancellation with Coroutine Scopes

Structured concurrency connects the lifecycle of child coroutines to their parent scopes. When a parent is cancelled, its child coroutines are cancelled as well. This helps prevent related work from continuing after its owner has stopped.

Android lifecycle-aware scopes

Scope Lifecycle behavior Typical use
viewModelScope Cancelled when the ViewModel is cleared. Loading screen data and performing ViewModel operations.
lifecycleScope Cancelled when its lifecycle owner is destroyed. Lifecycle-bound operations in an Activity or Fragment.
coroutineScope { } Child coroutines belong to the scope and must complete before it returns. Grouping related suspending tasks.
supervisorScope { } Provides a scope where one child failure does not automatically cancel sibling children. Managing independent child operations.

Choose a scope according to how long the work should remain active. Screen-related work generally belongs in a lifecycle-aware scope rather than an unmanaged global scope.

7. What Is Cooperative Cancellation?

Kotlin coroutine cancellation is cooperative. Calling cancel() requests cancellation, but coroutine code must reach a cancellation-aware suspension point or explicitly check whether cancellation has been requested.

Many suspending functions from kotlinx.coroutines, including delay() and withContext(), support cancellation. A long-running CPU loop that never suspends or checks its status may continue running even after cancellation is requested.

Example of cancellation-aware work

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        repeat(1000) { index ->
            ensureActive()
            println("Processing: $index")
        }
    }

    delay(10)
    job.cancelAndJoin()

    println("Work stopped")
}

The call to ensureActive() checks whether the coroutine's job is still active. If cancellation has been requested, it throws a CancellationException.

Useful cancellation checks

Function or property Purpose
ensureActive() Throws a cancellation exception if the current coroutine is no longer active.
isActive Checks whether the coroutine's job is active.
yield() Suspends briefly to give other coroutines an opportunity to run and checks cancellation.
cancel() Requests cancellation of a job or scope.
cancelAndJoin() Requests cancellation and waits for job completion.

8. Handling Cancellation with try-finally

When a coroutine is cancelled, cleanup code may still need to run. For example, an application may need to release a resource, close a stream, or reset temporary state. A finally block is useful for cleanup because it executes when the associated try block exits normally or exceptionally.

Example

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(10) { index ->
                println("Task: $index")
                delay(300)
            }
        } finally {
            println("Cleaning up resources")
        }
    }

    delay(700)
    job.cancelAndJoin()
}

When cancellation occurs at the delay suspension point, the coroutine exits the try block and executes the finally block.

Use resource-safe language constructs such as use for closeable resources where appropriate. Such constructs help ensure resources are closed even if an exception or cancellation occurs.

9. Understanding CancellationException

CancellationException is the exception type commonly used to signal coroutine cancellation. In most cases, it represents normal lifecycle control rather than an unexpected application failure.

Coroutine libraries recognize cancellation exceptions and use them to stop work. If your code catches this exception and silently ignores it, cancellation may no longer propagate correctly through the rest of the operation.

Incorrect cancellation handling

try {
    performLongRunningOperation()
} catch (exception: Exception) {
    // Incorrect: this may swallow CancellationException.
    println("Something failed")
}

The catch block above catches broad exceptions, including cancellation exceptions. If cancellation is swallowed, the code may continue running or report an incorrect result.

Better cancellation handling

import kotlinx.coroutines.CancellationException

suspend fun performTaskSafely() {
    try {
        performLongRunningOperation()
    } catch (exception: CancellationException) {
        throw exception
    } catch (exception: java.io.IOException) {
        println("I/O operation failed: ${exception.message}")
    }
}

suspend fun performLongRunningOperation() {
    // Replace this with a real suspending operation.
}

This example explicitly rethrows cancellation and handles an expected I/O failure separately. If no special cleanup is required, cancellation can often be allowed to propagate without catching it at all.

10. Introduction to Coroutine Exceptions

An exception is an event that interrupts normal program execution. In coroutine-based applications, failures can occur because of network problems, invalid input, unavailable files, database errors, parsing issues, or programming mistakes.

Exception handling should ensure that failures are handled at an appropriate level, that the UI displays meaningful feedback, and that cancellation continues to function correctly.

Common exception types

Exception Typical cause Recommended approach
CancellationException A coroutine has been cancelled. Normally allow it to propagate.
IOException Network or file I/O failure. Handle it where the application can recover or show an appropriate error.
HttpException An HTTP client reports an unsuccessful HTTP response, depending on the client library. Inspect the status and map it to a meaningful application error.
SerializationException Response data cannot be decoded as expected. Validate data and handle parsing failures.
IllegalStateException An operation is performed in an invalid state. Investigate the program logic and correct the underlying cause.

Some exception types depend on the library used by the application. For example, HTTP and serialization exceptions vary between Retrofit converters, JSON libraries, and other networking solutions.

11. Handling Exceptions with try-catch

The try-catch statement lets code handle an exception instead of allowing that exception to escape from the current operation. In coroutine-based Android code, it is commonly used inside a coroutine body to handle expected failures from suspending functions.

Example: Handling a network failure

import java.io.IOException
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

suspend fun fetchUser(): String {
    delay(500)
    throw IOException("Unable to connect to server")
}

fun main() = runBlocking {
    launch {
        try {
            val user = fetchUser()
            println(user)
        } catch (exception: CancellationException) {
            throw exception
        } catch (exception: IOException) {
            println("Network error: ${exception.message}")
        }
    }
}

The example simulates a failed network operation. The expected I/O error is handled, while cancellation is rethrown rather than being mistaken for an ordinary network failure.

Best practices for try-catch

  • Catch specific exceptions that you can handle meaningfully.
  • Keep the try block focused on operations that may fail.
  • Do not ignore exceptions without a clear reason.
  • Do not turn cancellation into a successful result.
  • Update application state when an operation fails.
  • Log useful diagnostic information without exposing passwords, tokens, or private user data.

12. Using finally for Cleanup

The finally block is useful when a task must perform cleanup regardless of success, failure, or cancellation.

Example

suspend fun downloadFile() {
    try {
        println("Starting download")
        // Perform the download here.
    } catch (exception: java.io.IOException) {
        println("Download failed: ${exception.message}")
        throw exception
    } finally {
        println("Releasing temporary download resources")
    }
}

This example shows the general cleanup pattern. In a production application, use the networking or file API's documented resource-management mechanisms and preserve cancellation behavior. Cleanup in finally should generally be short and non-blocking.

13. Exception Behavior of launch() and async()

The launch() and async() builders handle results and failures differently. Understanding their behavior is essential for correct exception handling.

Feature launch() async()
Return value Job Deferred<T>
Purpose Starts work without returning a direct result. Starts work that produces a result.
Failure In a regular parent-child hierarchy, a child failure generally cancels its parent unless supervision changes that behavior. The failure is stored in the Deferred result and is observed when its result is awaited; in a regular hierarchy, failure can still cancel the parent.
Typical handling Handle expected exceptions inside the coroutine body. Handle exceptions when calling await(), or inside the operation when appropriate.

Example using async and await

import kotlinx.coroutines.*

fun main() = runBlocking {
    val result = async {
        delay(300)
        100 / 0
    }

    try {
        println(result.await())
    } catch (exception: ArithmeticException) {
        println("Calculation failed: ${exception.message}")
    }
}

The calculation throws an arithmetic exception. Calling await() exposes the failed result to the caller. In a regular structured concurrency hierarchy, an exception from a child async can also cancel its parent and sibling coroutines; catching it at await() does not automatically undo parent cancellation that has already occurred.

14. Understanding CoroutineExceptionHandler

CoroutineExceptionHandler is a coroutine context element used to handle certain uncaught exceptions from coroutines. It is mainly useful for reporting or logging uncaught failures from appropriate root coroutines or supervised coroutines.

It is not a replacement for try-catch around operations that are expected to fail. It also cannot recover a coroutine that has already completed exceptionally.

Example

import kotlinx.coroutines.*

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Unhandled coroutine exception: ${exception.message}")
    }

    val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + handler)

    scope.launch {
        throw IllegalStateException("Unexpected failure")
    }.join()

    scope.cancel()
}

This example uses a supervised root scope to demonstrate reporting an uncaught exception. In an Android application, use an appropriate lifecycle or application scope and a deliberate error-reporting strategy rather than creating unmanaged scopes for ordinary screen work.

Important points

  • Use try-catch for failures that your application expects and can handle.
  • Use CoroutineExceptionHandler for appropriate uncaught exceptions that need reporting.
  • A handler does not prevent normal parent cancellation in a regular coroutine hierarchy.
  • Exceptions from async() should normally be observed through await().

15. Understanding Parent-Child Exception Propagation

Structured concurrency organizes coroutines into parent-child relationships. In a regular job hierarchy, an uncaught exception from a child coroutine generally cancels the parent, and the parent's cancellation propagates to its other children.

This behavior helps keep related tasks consistent. If a required operation fails, the application does not continue as though every part of the operation succeeded.

Example of exception propagation

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        launch {
            delay(300)
            println("Child task A completed")
        }

        launch {
            delay(100)
            throw IllegalStateException("Child task B failed")
        }
    }

    try {
        job.join()
    } catch (exception: CancellationException) {
        println("The job was cancelled")
    }
}

In this example, the child failure cancels the regular parent job and its sibling. Note that join() does not itself rethrow the original failure in the same way that await() exposes a Deferred result; the exact exception-reporting behavior depends on the coroutine hierarchy and builder. For reliable handling, catch expected failures inside the appropriate coroutine or use a supervision design when children are independent.

16. Using supervisorScope for Independent Tasks

Sometimes multiple child tasks are independent. For example, a dashboard may load weather, news, and account information separately. A failure in one request should not necessarily cancel all the other requests.

supervisorScope creates a scope in which a child failure does not automatically cancel its sibling children. However, if the supervisor scope itself fails or is cancelled, its children are cancelled.

Example

import kotlinx.coroutines.*

suspend fun loadDashboard() = supervisorScope {
    val weatherJob = launch {
        try {
            delay(300)
            println("Weather loaded")
        } catch (exception: CancellationException) {
            throw exception
        } catch (exception: Exception) {
            println("Weather loading failed")
        }
    }

    val newsJob = launch {
        try {
            delay(500)
            println("News loaded")
        } catch (exception: CancellationException) {
            throw exception
        } catch (exception: Exception) {
            println("News loading failed")
        }
    }

    weatherJob.join()
    newsJob.join()
}

Each child handles its own expected failure. The supervisor structure allows the other child to continue when one child fails independently. Avoid broad exception handling unless you have a clear policy for unexpected failures.

17. Handling Cancellation and Exceptions in an Android ViewModel

Android applications commonly use viewModelScope to launch work associated with a ViewModel. This scope is cancelled when the ViewModel is cleared, making it appropriate for screen-related operations.

The ViewModel can expose loading, success, and error states to the UI. Cancellation should normally propagate, while expected failures such as network errors should be converted into useful application state.

Example ViewModel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.io.IOException
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

sealed interface UserUiState {
    data object Loading : UserUiState
    data class Success(val userName: String) : UserUiState
    data class Error(val message: String) : UserUiState
}

class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    private val _uiState =
        MutableStateFlow<UserUiState>(UserUiState.Loading)

    val uiState: StateFlow<UserUiState> =
        _uiState.asStateFlow()

    fun loadUser() {
        viewModelScope.launch {
            _uiState.value = UserUiState.Loading

            try {
                val name = repository.fetchUserName()
                _uiState.value = UserUiState.Success(name)
            } catch (exception: CancellationException) {
                throw exception
            } catch (exception: IOException) {
                _uiState.value =
                    UserUiState.Error("Please check your connection and try again.")
            } catch (exception: Exception) {
                _uiState.value =
                    UserUiState.Error("Unable to load user information.")
            }
        }
    }
}

Note: This example assumes that UserRepository.fetchUserName() is a suspending function that may throw an IOException. The sealed interface syntax requires a compatible Kotlin version. If your project uses an older Kotlin version, adjust the state declarations accordingly.

How this implementation works

  1. The UI calls loadUser().
  2. The ViewModel starts a coroutine in viewModelScope.
  3. The state changes to Loading.
  4. The repository retrieves the user's name.
  5. On success, the state changes to Success.
  6. On a recognized network failure, the state changes to Error.
  7. Cancellation is rethrown so the coroutine's lifecycle behavior remains correct.

The UI can collect the StateFlow and display the correct state. In production, use your application's error model to distinguish retryable network errors, authentication failures, invalid responses, and unexpected failures.

18. Handling Cancellation During Resource Cleanup

Sometimes a cleanup operation must suspend, such as flushing buffered information or finishing a required resource operation. Because a cancelled coroutine cannot freely perform further cancellable suspending work, use NonCancellable only when a short, necessary suspending cleanup must complete despite cancellation.

Example

import kotlinx.coroutines.*

suspend fun performOperation() {
    try {
        delay(1000)
        println("Operation completed")
    } finally {
        withContext(NonCancellable) {
            delay(100)
            println("Required cleanup completed")
        }
    }
}

This is a demonstration of the mechanism, not a pattern to apply to every cleanup block. Keep non-cancellable sections short because they delay completion of cancellation. Never use them to keep an entire operation running after it is no longer needed.

19. Common Mistakes and Their Solutions

Mistake Problem Recommended solution
Swallowing CancellationException. Cancellation may stop propagating correctly. Allow it to propagate or rethrow it after necessary cleanup.
Using runBlocking on the main thread. The UI may freeze. Use lifecycle-aware scopes and suspending APIs.
Assuming cancellation instantly stops every operation. Non-cooperative code may continue executing. Use cancellable suspension points or explicit cancellation checks.
Catching every exception and ignoring it. Failures become invisible and application state may be incorrect. Handle expected exception types and report unexpected failures.
Using CoroutineExceptionHandler as a substitute for all error handling. Expected failures may not be handled at the right level. Use local try-catch for recoverable operations.
Launching unrelated tasks in a regular parent scope without considering failure propagation. One child failure can cancel other child tasks. Use supervisorScope when child operations are genuinely independent.
Performing long CPU loops without cancellation checks. Cancellation may be delayed. Use ensureActive() or yield() where appropriate.
Using NonCancellable for long operations. Cancellation can be delayed unnecessarily. Restrict it to short, essential suspending cleanup.

20. Best Practices

  • Use viewModelScope for work owned by a ViewModel.
  • Use lifecycleScope when work should be tied to an Activity or Fragment lifecycle.
  • Catch expected exceptions near the operation that can meaningfully handle them.
  • Do not swallow CancellationException.
  • Prefer specific exception types such as IOException instead of catching Throwable.
  • Use finally for cleanup and close resources safely.
  • Use structured concurrency to keep task lifecycles predictable.
  • Use supervisorScope when independent child tasks should be allowed to fail separately.
  • Keep UI state consistent by representing loading, success, and error states explicitly.
  • Log diagnostic details securely and avoid exposing sensitive information.
  • Test failure, cancellation, retry, and lifecycle scenarios rather than testing only successful operations.

21. Practical Exercises

Exercise 1: Cancel a background task

Create a coroutine that prints a message every 500 milliseconds. Cancel it after two seconds and verify that it stops and completes its cleanup.

Exercise 2: Handle an I/O exception

Create a suspending function that simulates a failed file or network operation by throwing an IOException. Handle the failure and display a suitable message.

Exercise 3: Preserve cancellation

Write a function that catches an expected network exception but rethrows CancellationException. Test the function by cancelling its job during a suspending operation.

Exercise 4: Compare launch and async

Run one operation using launch() and another using async(). Observe how their failures are reported and how await() exposes a Deferred result's exception.

Exercise 5: Build a resilient Android screen

Create a ViewModel that loads user data. Add loading, success, and error states. Ensure the request is cancelled when the ViewModel is cleared and that expected network failures produce a useful error state.

Mini project: Reliable Product Search

Build a product-search screen that sends requests as the user enters text. Cancel obsolete searches when a new query arrives, display loading and result states, handle network failures, and ensure cancellation is not displayed as a normal error. Verify that an older response cannot overwrite the results for a newer query.

22. Interview Questions and Answers

Q1. What is coroutine cancellation?

Coroutine cancellation is a request to stop coroutine work that is no longer required. It is commonly triggered through a Job or parent scope.

Q2. What is cooperative cancellation?

It means coroutine code responds to cancellation at cancellation-aware suspension points or when it explicitly checks its cancellation status.

Q3. What is CancellationException?

It is an exception commonly used to signal coroutine cancellation. It should normally be allowed to propagate rather than being swallowed.

Q4. What is the difference between cancel() and cancelAndJoin()?

cancel() requests cancellation. cancelAndJoin() requests cancellation and then suspends until the job completes.

Q5. Why is finally important in coroutines?

A finally block supports cleanup when an operation completes normally, fails, or is cancelled.

Q6. How should network exceptions be handled?

Catch expected network-related exceptions at a suitable layer and convert them into meaningful application states, while allowing cancellation exceptions to propagate.

Q7. What is CoroutineExceptionHandler?

It is a coroutine context element for reporting certain uncaught exceptions. It is not a replacement for local handling of expected failures.

Q8. How do launch and async differ in exception handling?

launch() reports uncaught failures through coroutine exception propagation, while async() stores its result or failure in a Deferred object, which exposes the failure through await(). In a regular hierarchy, either child can still cancel its parent when it fails.

Q9. What is supervisorScope?

supervisorScope lets child coroutines fail independently without automatically cancelling sibling children, although failure or cancellation of the scope itself cancels its children.

Q10. Why is viewModelScope useful?

It ties coroutine work to the ViewModel lifecycle and cancels its child coroutines when the ViewModel is cleared.

Q11. Can a coroutine always stop immediately when cancelled?

No. Cancellation is cooperative. Code that does not suspend or check for cancellation may continue until it reaches a suitable cancellation check.

Q12. When should NonCancellable be used?

It should be reserved for short, necessary suspending cleanup operations that must finish despite cancellation.

23. Quick Revision Checklist

  • A coroutine's lifecycle is represented by a Job.
  • Deferred is a Job that also provides a result.
  • cancel() requests cancellation.
  • cancelAndJoin() requests cancellation and waits for completion.
  • Cancellation is cooperative.
  • ensureActive() and yield() can help long-running work respond to cancellation.
  • CancellationException should normally propagate.
  • try-catch handles failures that can be recovered from.
  • finally is useful for cleanup.
  • CoroutineExceptionHandler handles certain uncaught exceptions but does not replace local error handling.
  • Regular child failures can cancel the parent and sibling coroutines.
  • supervisorScope supports independent child failure handling.
  • viewModelScope ties work to the ViewModel lifecycle.
  • UI state should represent loading, success, and error outcomes clearly.

24. Conclusion

Managing coroutine cancellation and exceptions is essential for building dependable Android applications. Cancellation prevents obsolete tasks from consuming resources, while correct exception handling helps applications recover from failures and communicate useful information to users.

By understanding cooperative cancellation, structured concurrency, CancellationException, try-catch-finally, CoroutineExceptionHandler, and supervisorScope, Android developers can create asynchronous workflows that are easier to maintain and test.

For additional reference, explore the official Kotlin coroutine cancellation documentation, the Kotlin coroutine exception-handling guide, and the Android coroutine best practices.

Explore the JustAcademy Android App Development Course, register through the course demo registration page, or visit the JustAcademy official website.

Previous Next
whatsapp