Every R6 class in kucoin 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(
kucoin[KucoinMarketData, KucoinTrading, KucoinAccount, get_api_keys],
coro[async, await],
later[run_now, loop_empty],
promises[then, catch, promise_all, promise_resolve]
)
keys <- get_api_keys(
api_key = "your-api-key",
api_secret = "your-api-secret",
api_passphrase = "your-api-passphrase"
)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
The most ergonomic way to work with promises in R is
coro::async, which lets you write code that looks
synchronous but runs asynchronously under the hood — just like
TypeScript’s async/await.
market <- KucoinMarketData$new(async = TRUE)
get_stats <- async(function() {
stats <- await(market$get_24hr_stats("BTC-USDT"))
return(stats)
})
get_stats()
while (!loop_empty()) 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, just like await in TypeScript:
results <- NULL
fetch_tickers <- async(function() {
btc <- await(market$get_ticker("BTC-USDT"))
eth <- await(market$get_ticker("ETH-USDT"))
results <<- list(btc = btc, eth = eth)
})
fetch_tickers()
while (!loop_empty()) run_now()
results$btc
#> datetime sequence price size best_bid best_bid_size
#> <POSc> <char> <char> <char> <char> <char>
#> 1: 2024-10-17 10:04:19 1550467636704 67232.9 0.00007682 67232.8 0.41861839
#> best_ask best_ask_size
#> <char> <char>
#> 1: 67232.9 1.24808993
results$eth
#> datetime sequence price size best_bid best_bid_size best_ask
#> <POSc> <char> <char> <char> <char> <char> <char>
#> 1: 2024-10-17 10:04:19 200001 2530.6 0.5 2530.5 12.0 2530.8
#> best_ask_size
#> <char>
#> 1: 8.5Concurrent Requests with promise_all
When requests are independent, fire them simultaneously and collect
all results at once — the async equivalent of Promise.all()
in TypeScript:
results <- NULL
fetch_parallel <- async(function() {
# Launch both requests concurrently — no await yet, just collect promises
btc_promise <- market$get_ticker("BTC-USDT")
eth_promise <- market$get_ticker("ETH-USDT")
# Await them together — like Promise.all([btc, eth])
res <- await(promise_all(btc = btc_promise, eth = eth_promise))
results <<- res
})
fetch_parallel()
while (!loop_empty()) run_now()
results$btc
#> datetime sequence price size best_bid best_bid_size
#> <POSc> <char> <char> <char> <char> <char>
#> 1: 2024-10-17 10:04:19 1550467636704 67232.9 0.00007682 67232.8 0.41861839
#> best_ask best_ask_size
#> <char> <char>
#> 1: 67232.9 1.24808993
results$eth
#> datetime sequence price size best_bid best_bid_size best_ask
#> <POSc> <char> <char> <char> <char> <char> <char>
#> 1: 2024-10-17 10:04:19 200001 2530.6 0.5 2530.5 12.0 2530.8
#> best_ask_size
#> <char>
#> 1: 8.5Promise Chaining with then / catch
If you prefer the promise-pipeline style (common in JavaScript), use
then and catch:
account <- KucoinAccount$new(async = TRUE)
chain_result <- NULL
account$get_summary() |>
then(function(summary) {
chain_result <<- summary
}) |>
catch(function(err) {
message("Error: ", conditionMessage(err))
})
while (!loop_empty()) run_now()
chain_result
#> level sub_quantity max_default_sub_quantity max_sub_quantity
#> <int> <int> <int> <int>
#> 1: 1 3 5 5
#> spot_sub_quantity margin_sub_quantity futures_sub_quantity
#> <int> <int> <int>
#> 1: 2 1 0
#> option_sub_quantity max_spot_sub_quantity max_margin_sub_quantity
#> <int> <int> <int>
#> 1: 0 5 5
#> max_futures_sub_quantity max_option_sub_quantity
#> <int> <int>
#> 1: 5 5Async Trading Example
Place a test order and immediately query open orders — sequential
await keeps the flow readable:
trading <- KucoinTrading$new(async = TRUE)
results <- NULL
place_and_check <- async(function() {
# Place a test order
order <- await(trading$add_order_test(
type = "limit",
symbol = "BTC-USDT",
side = "buy",
price = "50000",
size = "0.0001"
))
# Query open orders
open <- await(trading$get_open_orders("BTC-USDT"))
results <<- list(order = order, open = open)
})
place_and_check()
while (!loop_empty()) run_now()
results$order
#> order_id client_oid
#> <char> <char>
#> 1: 670fd33bf9406e0007ab3945 5c52e11203aa677f33e493fb
results$open
#> id symbol op_type type side price size funds
#> <char> <char> <char> <char> <char> <char> <char> <char>
#> 1: 670fd33bf9406e0007ab3945 BTC-USDT DEAL limit buy 50000 0.0001 0
#> deal_size deal_funds fee fee_currency stp time_in_force cancel_after
#> <char> <char> <char> <char> <char> <char> <int>
#> 1: 0 0 0 USDT GTC -1
#> post_only hidden iceberg visible_size cancelled_size cancelled_funds
#> <lgcl> <lgcl> <lgcl> <char> <char> <char>
#> 1: FALSE FALSE FALSE 0 0 0
#> remain_size remain_funds active in_order_book client_oid
#> <char> <char> <lgcl> <lgcl> <char>
#> 1: 0.0001 0 TRUE TRUE 5c52e11203aa677f33e493fb
#> tags last_updated_at datetime_created
#> <char> <num> <POSc>
#> 1: 1.729578e+12 2024-10-22 06:11:55Error Handling with tryCatch
Inside async functions, you can use
tryCatch around await calls for structured
error handling — just like try/catch in
TypeScript:
safe_fetch <- async(function() {
result <- tryCatch(
await(market$get_ticker("INVALID-PAIR")),
error = function(e) {
message("Caught error: ", conditionMessage(e))
return(NULL)
}
)
return(result)
})
safe_fetch()
while (!loop_empty()) run_now()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 <- lubridate::now() + 30 # 30-second timeout
while (!later::loop_empty() && lubridate::now() < 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.
For bots or long-running processes, use the Scheduler /
Looper pattern (see package README) where later
drives the loop automatically.
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. Return values from the async function and extract them viathen()or the<<-pattern outside the async body.
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 data downloads | Use kucoin_backfill_klines() (sync, handles batching
internally) |
Next Steps
- See
vignette("getting-started")for a full tour of all classes in sync mode. - Browse the pkgdown site for full method documentation.