Skip to content

Async / await

Vow ships a cooperative async runtime in 0.1.0. Use async fn + await for I/O and blocking work; call async code from sync route handlers with block_on.

HTTP handlers in vow-web-server stay fn(Request) -> Response. Put async work in helpers, then block_on(helper) inside the handler — same pattern as Express calling an async function from a sync wrapper.

From the hello-app demo:

async fn todo_count() -> int {
let rows = await db_query(g_handle, "SELECT id FROM todos")?;
let doc = json.parse(rows)?;
return json_len(doc)?;
}
fn api_health(_req: request.Request) -> response.Response {
if not db_ready() {
return vws.send(503, "database not ready");
}
return vws.json(json.stringify({
ok: true,
todos: block_on(todo_count),
}));
}
Terminal window
cd hello-app
docker compose up -d
vow run src/main.vow -- --grant db:postgres://localhost:5433/todos --grant net:
curl http://127.0.0.1:8787/api/health
Form Role
async fn name() -> T Async function — body may use await
await expr Suspend until the await target completes
expr? Propagate Result / Option errors (works inside async fn)
block_on(async_fn) Run async fn to completion from sync code
block_on(async_fn, arg1, …) Same, passing parameters to the async fn

Return types for block_on are inferred from the async function (int, String, Result<…>, etc.).

Await Use case
await db_query(handle, sql)? Database (offloaded to worker thread)
await read_file_async(path) Filesystem read
await async_sleep(ms) Timers / scheduling
await spawn_blocking(sync_fn) CPU or custom blocking fn() -> int
await other_async_fn() Compose no-arg async helpers
await async_spawn(child) Fire-and-forget child task

Async functions that call open_db() or other needs Db helpers must declare needs Db. The capability is passed from the sync caller via with db { block_on(init_db_work) } — the compiler stores the cap in the async frame before the worker runs.

fn open_db() -> Result<int64, int> needs Db {
return match vpg.connect(db_url()) {
Ok(c) => Ok(c.handle),
Err(e) => Err(e.code),
};
}
async fn init_db_work() -> int needs Db {
let h = open_db()?;
let rows = await db_query(h, "SELECT id, title, done FROM todos ORDER BY id")?;
g_handle = h;
g_db_ok = 1;
return 0;
}
fn init_db(caps: Caps) -> int {
let db = caps.db?; // Option? → Db
return with db { block_on(init_db_work) }; // propagates Db into async frame
}

Call init_db(caps) from main before starting the HTTP server.

Return a JSON String from the async helper; check for empty result in the sync handler:

async fn insert_todo_work(body: String) -> String {
let _doc = json.parse(body)?;
let rows = await db_query(g_handle, sql)?;
let parsed = json.parse(rows)?;
let row = json_at(parsed, 0)?;
return json.stringify({ ok: true, id: json.get_int(row, "id", 0), title: title, done: false });
}
fn create_todo(req: request.Request) -> response.Response {
let body = str_trim(vws.body(req, 65536));
let payload = block_on(insert_todo_work, body);
if len(payload) == 0 {
return vws.send(503, "database error");
}
return vws.json(201, payload);
}
fn heavy_compute() -> int {
return 42;
}
async fn load() -> int {
let a = await child_async();
let b = await spawn_blocking(heavy_compute);
return a + b;
}
fn main(_caps: Caps) -> int {
return block_on(load);
}

See tests/lang/test_async_general.sh (expects exit code 49).

async fn read_one(path: String) -> int needs FsRead {
return match await read_file_async(path) {
Ok(_s) => 1,
Err(_e) => 0,
};
}
async fn wait() -> int needs Clock {
let _ = await async_sleep(20);
return 1;
}

Grant caps as usual: --grant fs-read:/path, --grant clock:.

  1. Sync handler — parses request, returns response.Response
  2. Async helperawait db_query, await read_file_async, etc.
  3. block_on(helper) — bridges sync HTTP to async I/O

Handlers are not async fn themselves. The accept loop is single-threaded; async yields keep the process responsive while DB/file work runs on thread-pool workers.

Example Command
tests/lang/test_async_general.sh child + spawn_blocking + multi-await locals
tests/lang/test_async_db.sh await db_query + ?
tests/lang/test_async_needs_cap.sh needs Db propagated through block_on
tests/lang/test_option_block_on.sh caps.db? + with db { block_on(...) }
tests/lang/test_async_block_on_str.sh block_on returning String
tests/lang/async_join.vow async_spawn + join
tests/lang/async_scheduler.vow async_sleep
hello-app/src/main.vow Full-stack Postgres + HTTP
  • Not M:N — cooperative scheduler + pthread offload, not a full event-loop runtime
  • No network async await — HTTP client I/O is still sync
  • spawn_blocking — no-arg fn() -> int only today
  • vow-web-server — handlers are sync; use block_on inside them

See Concurrency for OS threads and Limitations.