The Future of Asynchronous Programming in R

August 1, 2025

Charlie Gao (Open Source - Posit Software, PBC)

Email


Polling

⏳📩🔄

  • Email client checks every [15] mins
  • Previously the norm, even now sometimes the only way

Push Notifications

📨

  • Event-driven, sent as soon as email arrives
  • More modern approach

mirai

https://mirai.r-lib.org/

mirai() runs R code in the background, without blocking the R session.

library(mirai)

m <- mirai({
  Sys.sleep(1)
  100 + 42
})

mirai - Polling

How to check for a result?


unresolved(m)
[1] TRUE

mirai - Polling

How to check for a result?


while (unresolved(m)) {
  Sys.sleep(0.1)
  # or do actual work
}
m$data
[1] 142


→ Like checking email every 15 mins.

mirai - What actually happens

C level: mirai object completion callback


background thread

  • Checks error code
  • Stores pointer to binary data
  • Stores completion status

R function: $data or unresolved()


main thread

  • Checks completion status
  • Checks if binary data is available
  • Unserializes this into an R object


→ Much more asynchronous.

→ Efficient by doing as much as possible early.

→ So, not quite like checking for email.

mirai - Event-driven


Use [] to wait for and collect the result:


m[]
[1] 142


→ This is efficient, but blocking.

Async toolkit

promises

https://rstudio.github.io/promises/


Higher level - interface for async

as.promise(x)$then(func)


later

https://later.r-lib.org


Lower level - implementation for async

later(func, secs)

Polling promises (the old way)

as.promise.mirai <- function(x) {
  promises::promise(
    function(resolve, reject) {
      check <- function() {
        if (unresolved(x)) {
          later::later(check, delay = 0.1)
        } else {
          value <- x$data
          if (is_error_value(value)) reject(value) else resolve(value)
        }
      }
      check()
    }
  )
}

→ Schedules itself to check every 0.1 secs via later.

Event-driven promises (the new way)

as.promise.mirai <- function(x) {
  promises::promise(
    function(resolve, reject) {
      if (unresolved(x)) {    
        .keep(x, environment())
      } else {
        value <- x$data
        if (is_error_value(value)) reject(value) else resolve(value)
      }
    }
  )
}

→ No repeating check.

.keep() is a special function which tells the mirai completion callback to call into later (via its C interface) to resolve the promise.

→ Actions are scheduled as soon as a mirai completes.

We’ve upgraded async across the ecosystem


             

Thanks

The Future of Asynchronous Programming in R

(we now have push notifications)