Skip to contents

Every R6 class in binance accepts an async = TRUE flag at construction. When enabled, all methods return promises::promise objects instead of direct values. This vignette shows how to consume those promises with coro::async/await and later::run_now.

Why Async?

Synchronous HTTP blocks the R session while waiting for a reply. Asynchronous mode lets you fire off multiple requests and process results as they arrive – useful for bots that poll several endpoints or place orders in parallel.

Setup

box::use(
  binance[BinanceMarketData, BinanceTrading, BinanceAccount, get_api_keys],
  coro[async, await],
  later[run_now, loop_empty],
  promises[then, catch, promise_all]
)

Event loop: R does not have a built-in event loop like Node.js or Python’s asyncio. Promises only resolve when the event loop ticks via later::run_now(). In scripts and vignettes, drain the loop with while (!loop_empty()) run_now(). In Shiny apps the event loop runs automatically.


Basic Async: coro::async + await

Pass async = TRUE to any class constructor. Methods then return promises instead of data.tables:

market <- BinanceMarketData$new(async = TRUE)

get_ticker <- coro::async(function() {
  ticker <- await(market$get_ticker(symbol = "BTCUSDT"))
  return(ticker)
})

get_ticker()
while (!later::loop_empty()) {
  later::run_now()
}

Key pattern: define an async function, await each API call, return the result. Drain the event loop with while (!loop_empty()) run_now().


Sequential Async: Multiple await Calls

Chain several awaited calls in sequence – each one resolves before the next begins:

market <- BinanceMarketData$new(async = TRUE)
results <- NULL

fetch_tickers <- coro::async(function() {
  btc <- await(market$get_ticker(symbol = "BTCUSDT"))
  eth <- await(market$get_ticker(symbol = "ETHUSDT"))
  results <<- list(btc = btc, eth = eth)
})

fetch_tickers()
while (!later::loop_empty()) {
  later::run_now()
}
results$btc
results$eth

Concurrent Requests with promise_all

When requests are independent, fire them simultaneously and collect all results at once – the async equivalent of Promise.all() in JavaScript:

market <- BinanceMarketData$new(async = TRUE)
results <- NULL

fetch_parallel <- coro::async(function() {
  # Launch both requests concurrently (no await yet)
  btc_promise <- market$get_ticker(symbol = "BTCUSDT")
  eth_promise <- market$get_ticker(symbol = "ETHUSDT")

  # Await them together
  res <- await(promises::promise_all(btc = btc_promise, eth = eth_promise))
  results <<- res
})

fetch_parallel()
while (!later::loop_empty()) {
  later::run_now()
}
results$btc
results$eth

Promise Chaining with then / catch

If you prefer the promise-pipeline style, use then and catch:

market <- BinanceMarketData$new(async = TRUE)
chain_result <- NULL

market$get_24hr_stats(symbol = "BTCUSDT") |>
  promises::then(function(stats) {
    chain_result <<- stats
  }) |>
  promises::catch(function(err) {
    message("Error: ", conditionMessage(err))
  })

while (!later::loop_empty()) {
  later::run_now()
}
chain_result

Practical Example: Fetching Multiple Symbols Concurrently

A common use case is polling price data for a watchlist of symbols. With async mode, all requests fly in parallel rather than sequentially:

market <- BinanceMarketData$new(async = TRUE)
symbols <- c("BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT")
all_tickers <- NULL

fetch_watchlist <- coro::async(function() {
  # Fire all requests concurrently
  ticker_promises <- lapply(symbols, function(sym) {
    market$get_ticker(symbol = sym)
  })
  names(ticker_promises) <- symbols

  # Await all at once
  res <- await(do.call(promises::promise_all, ticker_promises))
  all_tickers <<- res
})

fetch_watchlist()
while (!later::loop_empty()) {
  later::run_now()
}

# Each element is a data.table with the ticker result
all_tickers$BTCUSDT
all_tickers$ETHUSDT

Running the Event Loop

The critical piece of async R is the event loop. Promises do not resolve until the event loop ticks. In an interactive session or Shiny app, the event loop runs automatically. In scripts or vignettes, you must drive it manually.

# Idiomatic event loop drain
while (!later::loop_empty()) {
  later::run_now()
}

Or with a timeout guard:

deadline <- Sys.time() + 30  # 30-second timeout
while (!later::loop_empty() && Sys.time() < deadline) {
  later::run_now(timeoutSecs = 0.1)
}

In Shiny applications, the event loop is managed for you – simply return promises from reactive expressions and Shiny handles resolution.


Error Handling with tryCatch

Inside async functions, use tryCatch around await calls for structured error handling:

market <- BinanceMarketData$new(async = TRUE)

safe_fetch <- coro::async(function() {
  result <- tryCatch(
    await(market$get_ticker(symbol = "INVALIDPAIR")),
    error = function(e) {
      message("Caught error: ", conditionMessage(e))
      return(NULL)
    }
  )
  return(result)
})

safe_fetch()
while (!later::loop_empty()) {
  later::run_now()
}

coro::await Cheat Sheet

Pattern Works? Notes
x <- await(promise) Yes Standard pattern
x <- await(obj$method(arg)) Yes Await wrapping a call is fine
await(promise) (bare, no assignment) Yes Side-effect only
await inside loops/if/tryCatch Yes Full control flow support
x <<- await(promise) No <<- not supported by coro
f(await(promise)) No Nested inside function args

Rule of thumb: await() must appear as the RHS of a <- or as a bare statement – never inside another expression.


Choosing Sync vs Async

Scenario Recommendation
Interactive exploration Sync – simpler, results print immediately
Scripts fetching one endpoint Sync – no event loop needed
Bots polling multiple symbols Async – concurrent requests reduce latency
Shiny dashboards Async – keeps the UI responsive
Bulk kline downloads Sync – handle batching in a loop