Skip to contents

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 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

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 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, 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.5

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 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.5

Promise 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                       5

Async 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:55

Error 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 via then() 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