Skip to content

Instantly share code, notes, and snippets.

@twyatt
Last active December 1, 2020 20:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save twyatt/c61ebc97dd891ff7a888311df857f619 to your computer and use it in GitHub Desktop.
Save twyatt/c61ebc97dd891ff7a888311df857f619 to your computer and use it in GitHub Desktop.
try-finally vs. invokeOnCompletion

The invokeOnCompletion will always be invoked, but it has a number of different characteristics vs. its try counterpart.

try-catch-finally invokeOnCompletion
  • suspend functionality
  • Predictable threading/context
  • Familiar semantics
  • Adheres to Coroutine cancellation
  • Blocking execution on arbitrary thread
  • No guarantees around execution timing
  • Implementation must be fast, non-blocking, and thread-safe
  • Exceptions are wrapped with CompletionHandlerException1
GlobalScope.launch(Dispatchers.IO) {
    try {
        // todo
    } finally {
        println("finally: ${Thread.currentThread().name}") // executes in current Coroutine context
    }
}.apply {
    invokeOnCompletion {
        println("invokeOnCompletion: ${Thread.currentThread().name}") // executes on arbitrary thread
    }
}

Output:

finally: DefaultDispatcher-worker-1
invokeOnCompletion: main

tl;dr You are essentially forfeiting some Coroutines functionality by using a completion handler that executes outside the Coroutine scope.


This isn't to say that invokeOnCompletion shouldn't be used (it has its purpose) but it's a pretty narrow subset of cases, IMHO.

If you always want something to be invoked even when the Coroutine is cancelled (and never started) then it would be good to use invokeOnCompletion:

val job = GlobalScope.launch(Dispatchers.IO, start = CoroutineStart.LAZY) {
    try {
        // todo
    } finally {
        println("finally: ${Thread.currentThread().name}")
    }
}.apply {
    invokeOnCompletion {
        println("invokeOnCompletion: ${Thread.currentThread().name}")
    }
}
job.cancel()

Output:

invokeOnCompletion: main

1 To demonstrate item 1 in the table above:

val handler = CoroutineExceptionHandler { _, cause ->
    println("handler: $cause")
}
GlobalScope.launch(Dispatchers.IO + handler) {
    try {
        // todo
    } finally {
        error("launch")
    }
}.apply {
    invokeOnCompletion {
        error("invokeOnCompletion")
    }
}

Output:

handler: java.lang.IllegalStateException: launch
handler: kotlinx.coroutines.CompletionHandlerException: Exception in completion handler InvokeOnCompletion[InvokeOnCompletion@4f5e20] for StandaloneCoroutine{Cancelled}@e6fff40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment