Pierre
Pierre

Reputation: 490

JobCancellationException StandaloneCoroutine was cancelled

Since we are using Coroutines (1.3.5 used) we have a lot of crash : JobCancellationException - StandaloneCoroutine was cancelled.

I read a lot of thread about theses problems and I tried a lot of solution in production but crashes always occurs.

In all our viewmodels we are using the viewmodelscope so it's ok.

But in our data layer we need to launch a tracking events which are fire and forget task. In first step we used a GlobalScope.launch. I was thinking the CancelletationException was due to this global scope so I removed it and create an extension in the data layer with using a SupervisorJob and a CoroutineExceptionHandler:

private val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private val coroutineExceptionHandler by lazy { CoroutineExceptionHandler { _, throwable -> logw("Error occurred inside Coroutine.", throwable) } }

fun launchOnApp(block: suspend CoroutineScope.() -> Unit) {
    appScope.launch(coroutineExceptionHandler) { block() }
}

But I always saw crashes with this code. Do I need to use cancelAndJoin method? Which strategy I can use with a clean archi and this kind of work please?

Thanks in advance

Upvotes: 24

Views: 18368

Answers (2)

Jaeha Yu
Jaeha Yu

Reputation: 139

I recommend not to use GlobalScope for the following reasons:

This is the description in CoroutineScope.kt : This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScope is used. A coroutine launched in GlobalScope is not subject to the principle of structured concurrency, so if it hangs or gets delayed due to a problem (e.g. due to a slow network), it will stay working and consuming resources.

There are limited circumstances under which GlobalScope can be legitimately and safely used, such as top-level background processes that must stay active for the whole duration of the application's lifetime. Because of that, any use of GlobalScope requires an explicit opt-in with @OptIn(DelicateCoroutinesApi::class)

// A global coroutine to log statistics every second, must be always active
@OptIn(DelicateCoroutinesApi::class)
val globalScopeReporter = GlobalScope.launch {
    while (true) {
        delay(1000)
        logStatistics()
    }
}

If you don't mind the job being canceled you can just ignore it.

To manage tasks that have been canceled or should not be undone, you need to know where your code is coming from and improve it.

var job: Job? = null
fun requestJob(from:String) {
    Log.send("test : from = $from")
    if (job != null) {
        job?.cancel()
        Log.d("test", "test : canceled")
    }

    job = GlobalScope.launch {
        (0..10).forEach { i ->
            delay(1000)
            Log.d("test", "test : job $i")
        }
    }.apply {
        invokeOnCompletion {
            Log.d("test", "test : from = $from, reason = ${it?.message ?: "completed"}")
            job = null
        }
    }
}

Upvotes: 0

Simone
Simone

Reputation: 1548

You can build an extension utility that catches the cancellation exception, and do what you want with it:

fun CoroutineScope.safeLaunch(block: suspend CoroutineScope.() -> Unit): Job {
  return this.launch {
    try {
      block()
    } catch (ce: CancellationException) {
      // You can ignore or log this exception
    } catch (e: Exception) {
      // Here it's better to at least log the exception
      Log.e("TAG","Coroutine error", e)
    }
  }
}

And you can use the extension with a coroutine scope of your choice, for example the global scope:

GlobalScope.safeLaunch{
  // here goes my suspend functions and stuff
}

or any viewmodel scope:

myViewModel.viewModelScope.safeLaunch{
// here goes my suspend functions and stuff
}

Upvotes: 13

Related Questions