Sitemap

Fire and Forget with Kotlin Coroutines

Call a suspending function without waiting for it to finish

4 min readMar 26, 2025

--

Photo by Arny Mogensen on Unsplash

How do you run a suspending function as a fire-and-forget background task, without waiting for it to finish running or return a value?

With structured concurrency, you don’t just forget a running task. Instead, if you don’t want to wait for it right here and now, you need to hand it over to a new owner. Making sure that your coroutines don’t get forgotten is exactly how Kotlin’s coroutine scopes fix memory leaks and avoid common bugs.

Scopes and channels are your tools to decide which part of your app owns each task. Let’s walk through the problem and the simple solution.

When you call a suspending function, your code stops to wait for your asynchronous outside operation to finish. That’s what suspend means.

suspend fun main() {
delay(1000)
println("Done waiting!")
}

It’s a big change from programming with callbacks, where you just send asynchronous tasks off to run in the background. Even async functions in languages like JavaScript can run as fire-and-forget background tasks if you choose to skip the await keyword.

To run a suspending function as a background task, you launch { … } it as a new coroutine. To do that, you have to provide a coroutine scope. The easiest way to get one is the coroutineScope { … } function—but if you try that out, you’ll find it still waits.

suspend fun main() {
coroutineScope {
launch { delay(1000) }
}
println("Done waiting!")
}

The coroutineScope { … } function is great when you want to run several tasks concurrently, then wait for them all to finish, but it’s no good for sending work off to run in the background. It always suspends to wait for all its child coroutines to complete their work before it returns.

Don’t leak errors and resources

Why is it so hard to just run a suspending function in the background?

Kotlin is making sure that errors and resources don’t get forgotten. If a coroutine crashes, it needs a parent task to send the exception back to. And when the parent task goes away, the coroutine should be cancelled to release resources and memory back to the rest of the app.

It’s tempting to work around this by using something like GlobalScope, or even by creating a custom scope. But depending on how you do that, you could be giving up all those safety mechanisms entirely.

Just because you want to fire and forget a task, that doesn’t mean it’s safe to let it run indefinitely, or for its errors to be completely ignored. It just means you don’t want to handle those things right here in the current function.

When that happens, the solution is to identify an alternative owner for the task. That might be another task or function higher up the call stack, or it might be a custom scope that you’ll create and cancel in another class.

Send it with a Channel

If you have a reference to the outside scope, you can call its launch function directly—but a Channel in the middle can give you the freedom and versatility to send tasks and messages in more ways to more places.

val tasks = Channel<suspend () -> Unit>()

suspend fun main() = coroutineScope {
launch { // run tasks in the background
for (task in tasks) launch { task.invoke() }
}
someOtherCode()
}

fun someOtherCode() {
tasks.trySend { // send a task to run in the background
delay(1000)
println("Done waiting!")
}
println("Fired but not forgotten…")
}

The code that sent the task to the channel doesn’t have to do any waiting, but the background work isn’t forgotten. It’s still a child of the application’s parent scope. It’ll be cancelled when you leave this part of the app, and if it fails, you’ll find out about the error.

Send your tasks to a coroutine scope that matches their intended lifecycle. To pick the right scope, consider which part of the app should be notified about a failed task, and which user actions or lifecycle events should cause the task to stop running and give up its resources for use elsewhere.

Instead of fire and forget, think fire and hand over.

Keep making your coroutines more idiomatic with my upcoming book, Kotlin Coroutine Confidence. Grab your early access beta copy today to be part of the book’s development and get a head start on untangling your async!

--

--

Responses (1)