---
title: "Understanding SupervisorJob in Kotlin Coroutines"
description: "In this article, you will learn what SupervisorJob is, how it works under the hood, and explored real-world use cases from viewModelScope and the RevenueCat SDK."
language: "en"
publishedAt: "2025-10-06T04:22:09Z"
updatedAt: "2025-10-06T04:22:09Z"
authors:
  - name: "Jaewoong Eum"
    url: "https://www.revenuecat.com/blog/author/jaewoong-eum"
category: "Engineering"
categoryUrl: "https://www.revenuecat.com/blog/engineering"
readingTime: 5
canonical: "https://www.revenuecat.com/blog/engineering/supervisorjob-kotlin"
---

# Understanding SupervisorJob in Kotlin Coroutines

In this article, you will learn what SupervisorJob is, how it works under the hood, and explored real-world use cases from viewModelScope and the RevenueCat SDK.

## Table of contents

- [Understanding the core difference: Job vs. SupervisorJob](#understanding-the-core-difference-job-vs-supervisorjob)
- [Internal mechanisms of SupervisorJob](#internal-mechanisms-of-supervisorjob)
  - [The supervisorScope builder](#the-supervisorscope-builder)
- [Real-world use cases](#real-world-use-cases)
- [Conclusion](#conclusion)

Coroutines are a powerful, language-level feature in Kotlin for asynchronous programming, and they’ve become the official solution across Android, Kotlin Multiplatform, and even backend development.

Coroutines are lightweight alternatives to threads. They can suspend without blocking system resources, making them highly efficient and well-suited for fine-grained concurrency. Kotlin provides useful coroutine APIs, such as builders, coroutine contexts, `Job`, and `Dispatchers`, that give you sophisticated ways to manage complex and nuanced concurrency scenarios.

In this article, you’ll dive into one of these concepts, `SupervisorJob`, exploring how it works, the internal mechanisms behind it, and how the [RevenueCat SDK for Android](https://github.com/RevenueCat/purchases-android) leverages it to handle asynchronous programming.

## **Understanding the core difference: Job vs. SupervisorJob**

In Kotlin’s structured concurrency, the default behavior of a `Job` is to enforce a “one-for-all, all-for-one” policy. When any child coroutine in a scope fails with an exception, it immediately cancels its parent, which in turn cancels all other sibling coroutines. This is a safe and predictable default for many use cases, but it’s not always desirable.

The `SupervisorJob` is a specialized type of `Job` designed to break this rigid failure propagation. Its purpose is to create a scope where children can fail independently without affecting the supervisor job itself or its other children. This enables fault isolation, an important pattern for building resilient applications where one failing task should not bring down the entire system.

Let’s see what are the differences between a `Job` and `SupervisorJob` :

`Job` (the default) enforces strict structured concurrency. Failure is propagated upwards. If a child coroutine launched within a `coroutineScope` or `CoroutineScope(Job())` fails with an exception, it cancels its parent job. The parent then immediately cancels all of its other children.

```kotlin
val scope = CoroutineScope(Job())
scope.launch { \/* Child 1 *\/ }
scope.launch { throw Exception("Child 2 Failed!") } \/\/ This will cancel the parent Job
scope.launch { \/* Child 3 *\/ } \/\/ This will be cancelled too
```

`SupervisorJob` (the resilient parent) modifies the cancellation behavior. Failure is not propagated upwards. If a child coroutine launched within a `CoroutineScope(SupervisorJob())` fails, it does not affect the parent `SupervisorJob` or any of its other children. The failure is isolated to that specific child.

```kotlin
val scope = CoroutineScope(SupervisorJob())
scope.launch { \/* Child 1 will keep running *\/ }
scope.launch { throw Exception("Child 2 Failed!") } \/\/ This failure is isolated
scope.launch { \/* Child 3 will keep running
```

## Internal mechanisms of SupervisorJob

If you look into the API surface of `SupervisorJob(parent)` function is a simple factory that instantiates a private implementation class, `SupervisorJobImpl`.

```kotlin
@Suppress("FunctionName")
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)

private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
    override fun childCancelled(cause: Throwable): Boolean = false
}
```

The entire implementation of `SupervisorJobImpl` is a single-line override. To understand this, we must first understand what it is overriding.

A standard `Job`, created via `Job()`, is an instance of `JobImpl`, which inherits its behavior from `JobSupport`. In `JobSupport`, when a child coroutine fails, it eventually calls `childCancelled(cause)` on its parent’s `ChildHandle`. The default implementation of this method in a standard `Job` is designed to propagate the failure upwards:

```kotlin
\/\/ In a regular JobSupport instance
public open fun childCancelled(cause: Throwable): Boolean {
    \/\/ A CancellationException is considered normal, don't cancel the parent.
    if (cause is CancellationException) return true

    \/\/ For any other exception, cancel the parent itself.
    return cancelImpl(cause)
}
```

This is the mechanism of failure propagation. A non-cancellation exception from a child causes the parent to cancel itself.

On the other hand, `SupervisorJobImpl` overrides this method with a single, simple statement:

```kotlin
override fun childCancelled(cause: Throwable): Boolean = false
```

It looks very simple, but it behaves more than just simple:

- The Override: By always returning `false`, the `SupervisorJob` is telling the coroutine machinery: “A child has notified me of its failure, but I have not handled the exception, and I will not be cancelling myself because of it.”
- The Effect: This effectively breaks the upward propagation of failure. The child’s failure is contained. The supervisor job remains active, and its other children continue to run unaffected. The responsibility for handling the failed child’s exception is now delegated elsewhere, typically to a `CoroutineExceptionHandler` in the child’s context or by the consumer of an `async` `Deferred` result.
This small but profound change is the entire internal mechanism that defines a `SupervisorJob`.

### The `supervisorScope` builder

The `supervisorScope` function provides a convenient way to create an ad-hoc supervisory scope. Its internal mechanism is to create a temporary, specialized coroutine, `SupervisorCoroutine`, which acts as the root of the new scope.

```kotlin
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
    \/\/ ...
    val coroutine = SupervisorCoroutine(uCont.context, uCont)
    \/\/ ...
}

private class SupervisorCoroutine<in T>(...) : ScopeCoroutine<T>(...) {
    override fun childCancelled(cause: Throwable): Boolean = false
}
```

Just like `SupervisorJobImpl`, the `SupervisorCoroutine` is a specialized coroutine whose only unique feature is that it overrides `childCancelled` to return `false`. When you launch new coroutines inside a `supervisorScope` block, their parent `Job` will be this `SupervisorCoroutine`. Therefore, any failures in those children will be stopped at the `SupervisorCoroutine`, preventing the failure from leaking out and cancelling the outer scope that called `supervisorScope`.

## Real-world use cases

The [RevenueCat Purchases SDK](https://github.com/RevenueCat/purchases-android) manages in-app subscriptions, demonstrating this principle perfectly through its strategic use of `SupervisorJob` in its `BlockstoreHelper` class. The `BlockstoreHelper` has a clever and specific job: it interacts with [Google’s Block Store API](https://developer.android.com/identity/block-store) to back up and restore an anonymous user ID. This is a user-friendly feature designed to help users seamlessly restore their purchases on a new device, even if they haven’t created an account.

An exception thrown during a Block Store operation must not be allowed to propagate and crash the host application, so the `BlockstoreHelper` achieves this necessary isolation by defining its own dedicated `CoroutineScopes`, each configured with a `SupervisorJob`.

```kotlin
internal class BlockstoreHelper
constructor(
    \/\/ ... dependencies
    private val ioScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO.limitedParallelism(1)),
    private val mainScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main),
) {
    \/\/ ...
}
```

So, any suspend function running inside `ioScope` or `mainScope` won’t propagate failures to other requests within the same scope, allowing them to continue running safely.

Another great example of use cases of `SupervisorJob` is the `viewModelScope` in Android `ViewModel`. A `ViewModel` often manages multiple, independent, and long-running tasks that should not interfere with each other.

Imagine a user profile screen in a social media app. The `ViewModel` needs to:

1. Fetch the user’s profile information.
1. Observe a real-time Flow of incoming messages.
1. Listen for updates to the user’s “online” status from another Flow.
These are three independent operations. If the message Flow fails due to a network blip, you certainly don’t want to stop fetching the user’s profile or listening for their online status.

You’re probably already using `viewModelScope` naturally and without any doubts, often following official examples like the ones shown below:

```kotlin
class ProfileViewModel(
    private val profileRepository: ProfileRepository,
    private val chatRepository: ChatRepository,
    private val statusRepository: StatusRepository
) : ViewModel() {

    \/\/ viewModelScope uses a SupervisorJob by default!
    \/\/ It's equivalent to CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)

    fun fetches() {
        \/\/ Task 1: Fetch profile data
        viewModelScope.launch {
            val profile = profileRepository.fetchUserProfile()
            \/\/ ..
        }

        \/\/ Task 2: Observe incoming messages
        viewModelScope.launch {
            val messages = chatRepository.getIncomingMessages()
            \/\/ ..
        }

        \/\/ Task 3: Observe online status
        viewModelScope.launch {
            val status = statusRepository.getOnlineStatus()
            \/\/ ..
        }
    }
}
```

The `viewModelScope` extension property wisely uses a `SupervisorJob` under the hood since the purpose of `ViewModel` is clear. If the `getIncomingMessages` suspend function throws an unhandled exception, only that specific `launch` block will fail. The other two coroutines, fetching the profile and observing the online status, will continue to run unaffected. Without a `SupervisorJob`, a single network error in the chat system would crash the entire `ViewModel`‘s scope, potentially leaving the UI in an inconsistent and unresponsive state.

If you explore the internal codes of `viewModelScope`, you will notice it’s already using the `SupervisorJob`:

```kotlin
public val ViewModel.viewModelScope: CoroutineScope
    get() =
        synchronized(VIEW_MODEL_SCOPE_LOCK) {
            getCloseable(VIEW_MODEL_SCOPE_KEY)
                ?: createViewModelScope().also { scope ->
                    addCloseable(VIEW_MODEL_SCOPE_KEY, scope)
                }
        }
        
internal fun createViewModelScope(): CloseableCoroutineScope {
    val dispatcher =
        try {
            Dispatchers.Main.immediate
        } catch (_: NotImplementedError) {
            EmptyCoroutineContext
        } catch (_: IllegalStateException) {
            EmptyCoroutineContext
        }
    return CloseableCoroutineScope(coroutineContext = dispatcher + SupervisorJob()) \/\/ here!
}
```

## Conclusion

In this article, you’ve learned what `SupervisorJob` is, how it works under the hood, and explored real-world use cases from `viewModelScope` and the RevenueCat SDK. By understanding its unique failure-handling behavior, you can design coroutine hierarchies that are more resilient, preventing one failing child from cancelling an entire scope.

As always, happy coding!

— **[Jaewoong](https://github.com/skydoves)**

---

## Related posts

- [Exploring Modifier.Node for creating custom Modifiers in Jetpack Compose](https://www.revenuecat.com/blog/engineering/compose-custom-modifier)
- [remember vs rememberSaveable: deep dive into state management and recomposition in Jetpack Compose](https://www.revenuecat.com/blog/engineering/remember-vs-remembersaveable)
- [Mark your models as stable with the Compose runtime annotation library](https://www.revenuecat.com/blog/engineering/compose-runtime-annotation)
