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 vialater::run_now(). In scripts and vignettes, drain the loop withwhile (!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
asyncfunction,awaiteach API call, return the result. Drain the event loop withwhile (!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$ethConcurrent 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$ethPromise 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_resultPractical 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$ETHUSDTRunning 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 |