I’ve noticed that often a lot of similar questions get asked within a few days of each other. Recently those questions have been about “fire-and-forget”, how it’s done in Cats Effect, and what issues one should be aware of. So I wrote up a little post with some background and techniques. Hope you enjoy it!

If you want to learn more about Cats Effect and concurrency, check out my book, Essential Effects!

What is “fire-and-forget”?

To understand fire-and-forget, let’s first distinguish it from a more typical method or function call whose purpose is to compute a value: a synchronous computation. We invoke the function, and can proceed once the value is produced.

def doSomething(i: Int): String = {
println(s"[${Thread.currentThread.getName}] doSomething($i)")
s"$i is a very nice number"
}

def doSomethingElse(): Int = {
println(s"[${Thread.currentThread.getName}] doSomethingElse()")
12
}

val doBoth = {
val result = doSomething(12)
println(s"[${Thread.currentThread.getName}] doSomething(12) produced: $result")
doSomethingElse
}

Notice: all println outputs show the same thread.

Next let’s break down fire-and-forget. What’s fire?
It means to start a computation without waiting for its result, along with a way to access its eventual result: an asynchronous computation. (The term fire comes from the military, where a missile is launched. Eek!)

What would the type signature of such an asynchronous thing be? It requires the necessary inputs, but immediately returns to the caller. We’re not returning the “actual” result of the computation–we need to merely signal “ok, we’re starting, here is a value that lets you act on the eventual result”. We’ll model this as a trait with a fire method that returns an “eventual result” signal (as a function):

trait Asynchronous {
type EventualResult[A] = () => A

def fire[A](run: => A): EventualResult[A]
}

We can implement this interface using Scala’s Future type:

object Asynchronous {
import scala.concurrent._
import scala.concurrent.duration._

val global =
new Asynchronous {
implicit val ec = scala.concurrent.ExecutionContext.global

def fire[A](run: => A): EventualResult[A] = {
val res = Promise[A].completeWith(Future(run)) // start the computation
() => Await.result(res.future, Duration.Inf) // wait on demand
}
}
}

We then convert our synchronous code to use our Asynchronous interface:


val doBothAsync = {
val await = Asynchronous.global.fire(doSomething(12))
val result = await()
println(s"[${Thread.currentThread.getName}] doSomething(12) produced: $result")
doSomethingElse
}

Notice: The println for doSomething happens in a different thread than doSomethingElse.

Finally, we need to forget. That is, after we start the computation (fire), we don’t do anything with our handler to the eventual result. We “forget” it:

trait Asynchronous {
type EventualResult[A] = () => A

def fire[A](run: => A): EventualResult[A]

def fireAndForget[A](run: => A): Unit =
fire(run) // ignore the eventual result handler
}
object Asynchronous {
import scala.concurrent._
import scala.concurrent.duration._

val global =
new Asynchronous {
implicit val ec = scala.concurrent.ExecutionContext.global

def fire[A](run: => A): EventualResult[A] = {
val res = Promise[A]().completeWith(Future(run)) // start the computation
() => Await.result(res.future, Duration.Inf) // wait on demand
}
}
}

The fireAndForget method invokes fire, which starts the asynchronous computation, but then ignores the returned EventualResult, meaning we won’t wait for any result at all.

Let’s update our example:

val doBothFireAndForget = {
Asynchronous.global.fireAndForget(doSomething(12))
doSomethingElse
}

Notice: we see the same asynchronous execution as the “fire”-only example, but don’t print out any intermediate results because we choose to not to wait for them.

Translating to Cats Effect

In Cats Effect, there are three parts to fire-and-forget:

  1. Define the synchronous computation as an effect.
  2. Fork the execution of the effect so it runs asynchronously (fire).
  3. Ignore the return value of the fork (forget).

Let’s do it:

import cats.effect._
import cats.implicits._

class Something extends IOApp {

def run(args: List[String]): IO[ExitCode] =
doBoth.as(ExitCode.Success)

def doSomething(i: Int): IO[String] = // <1>
for {
_ <- IO(println(s"[${Thread.currentThread.getName}] doSomething($i)"))
} yield s"$i is a very nice number"

def doSomethingElse(): IO[Int] =
for {
_ <- IO(println(s"[${Thread.currentThread.getName}] doSomethingElse()"))
} yield 12

val doBoth: IO[Int] =
for {
_ <- doSomething(12).start // <2> <3>
i <- doSomethingElse
} yield i
}

new Something().main(Array.empty)
  1. We change the signature from returning String to returning IO[String], to signal the definition of a (synchronous) effect.
  2. We start the IO effect value, forking its execution. It returns an IO[Fiber[IO, String]] (IO[FiberIO[String]] in Cats Effect 3) that lets you manage the asynchronous execution and eventual result.
  3. We also ignore the Fiber produced by start by “anonymizing” its name as _.

Is it safe to “forget”?

When we fire-and-forget using Cats Effect, we’re deliberately not keeping track of the forked computation. Is there a cost to this–is it safe?

To answer, we need to recall what we can do with the fiber of the forked effect: we can wait for its completion (via join) or cancel it (via cancel). Since we really want asychronous execution, we will ignore join, leaving cancel: would it ever be necessary to cancel a computation that we fired and forgot?

It certainly could be important to do so! Consider a long-lived task, like an event-handling loop. We might think “oh, that effect runs forever, it doesn’t need to be canceled”, and therefore 🚀 + 🙈: fire-and-forget. But cancelation might be needed! That forked effect exists in some context; it might be important to ensure that it is cancelled if its “parent” context is cancelled:

val businessLogic =
for {
_ <- longLivedEffect.start
_ <- doSomeOtherStuff
} yield ()

In this example perhaps longLivedEffect should be cancelled when businessLogic completes, but fire-and-forget precludes that possibility. We could instead link the lifetime of that long-lived effect to the surrounding business logic. One way is to use guarantee:

val businessLogic =
for {
fiber <- longLivedEffect.start
_ <- doSomeOtherStuff.guarantee(fiber.cancel) // <1>
} yield ()
  1. Cancel the fiber once the other logic completes successfully, with an error, or is cancelled.

But using guarantee doesn’t scale to multiple effects–for example, we want the fiber to be cancelled if any subsequent effect inside businessLogic is cancelled. We can instead model the lifecycle of an effect as a resource: a data structure that allows us to define a static region where state is used, along with effects for state acquisition and release:

val longLived: Resource[IO, IO[Unit]] =
longLivedEffect.background // <1>

val businessLogic =
longLived.use { join => // <2>
doSomeOtherStuff
}
  1. Instead of calling start to fork the execution, background returns a Resource that manages the forked fiber for you. The managed state is itself an action–an IO[Unit] that lets you join the underlying fiber (if you want to).
  2. Once we have a Resource, we use it to gain access to the managed state, where we can then perform our own effects. This defines a static region where the state is available, and outside this region we are guaranteed the state has been properly acquired and released. In the case of the resource returned by background, the state acquisition effect is “fork a fiber” and the release effect is “cancel the fiber”.

Summary

The pattern of fire-and-forget is really built out of three concepts:

  1. defining synchronous effects;
  2. forking an effect to run asynchronously; and
  3. controlling the asynchronously executing effect, either waiting for its eventual result or canceling it.

If you have short-lived effects that you want to run asynchronously, by all means fire them off and forget about them! But also know that execution is always in some context, and you have techniques like background to make the asynchronous lifecycle more visible.