Appearance
Async Tasks, Promises & Channels
Fresh 🌱The single most important rule in a native Node module: never block the JavaScript event loop. Neon gives you three cooperating tools to run work off the main thread and deliver results back safely:
Channel- schedule a closure to run back on the main JS thread from any thread.Promise/Deferred- give JavaScript a promise now, settle it later from Rust.- Tasks - run a closure on the libuv worker pool and resolve a promise with its result.
The golden rule
flowchart LR
MAIN["Main thread (JS event loop)"] -->|spawn / task| WORK["Background work (Rust)"]
WORK -->|Channel.send / Deferred.settle| MAIN
MAIN -->|resolve| PROM["JS Promise"]
style MAIN fill:#10b981,color:#fff
style WORK fill:#6366f1,color:#fffJavaScript values may only be created or touched on the main thread. Background threads do pure Rust work, then hand a closure back to the main thread (via a Channel) to do the JS part.
Worker-pool tasks (the easy path)
cx.task() runs a closure on Node's worker pool and returns a Promise. The first closure runs on a worker thread; the .promise() closure runs on the main thread with the result:
rust
use neon::prelude::*;
fn compute_async(mut cx: FunctionContext) -> JsResult<JsPromise> {
let input: f64 = cx.argument::<JsNumber>(0)?.value(&mut cx);
let promise = cx
.task(move || {
// Runs on a worker thread - safe to do heavy CPU work here
(0..1_000_000).map(|i| (i as f64 * input).sin()).sum::<f64>()
})
.promise(|mut cx, result| {
// Runs back on the main thread with the worker's result
Ok(cx.number(result))
});
Ok(promise)
}The same thing with the #[neon::export(task)] macro, which handles the promise plumbing:
rust
#[neon::export(task)]
fn heavy_computation(iterations: u32) -> u64 {
// Automatically runs on the worker pool and returns a Promise
(0..iterations as u64).sum()
}Promises you settle yourself
cx.promise() returns a (Deferred, Promise) pair. Hand the promise to JavaScript immediately; settle it whenever your work finishes - even from another thread.
rust
use neon::prelude::*;
// Resolve immediately
fn create_promise(mut cx: FunctionContext) -> JsResult<JsPromise> {
let (deferred, promise) = cx.promise();
let value = cx.string("done");
deferred.resolve(&mut cx, value);
Ok(promise)
}
// Resolve from a background thread using a Channel
fn async_operation(mut cx: FunctionContext) -> JsResult<JsPromise> {
let input: String = cx.argument::<JsString>(0)?.value(&mut cx);
let channel = cx.channel();
let (deferred, promise) = cx.promise();
std::thread::spawn(move || {
let result = input.to_uppercase(); // pure Rust work off the main thread
// Settle the promise back on the main thread
deferred.settle_with(&channel, move |mut cx| {
Ok(cx.string(result))
});
});
Ok(promise)
}When the value can convert to JS on its own, deferred.settle() is even shorter:
rust
fn promise_with_number(mut cx: FunctionContext) -> JsResult<JsPromise> {
let channel = cx.channel();
let (deferred, promise) = cx.promise();
std::thread::spawn(move || {
let result = 42.0f64;
deferred.settle(&channel, result);
});
Ok(promise)
}Channels: scheduling JS work from any thread
A Channel (obtained with cx.channel()) is the bridge. From any thread, channel.send(closure) schedules closure to run on the main JS thread, where it receives a fresh context and can safely touch JavaScript:
rust
let channel = cx.channel();
std::thread::spawn(move || {
// ... background work ...
channel.send(move |mut cx| {
// Back on the main thread: create JS values, call callbacks, etc.
Ok(())
});
});Combine Channel with a Root to invoke a JavaScript callback that was passed in earlier.
Async exports with futures
With the futures feature enabled, #[neon::export] can take an async fn. The async body runs on the runtime and the export returns a JavaScript Promise:
toml
# Cargo.toml
neon = { version = "1.1", features = ["futures"] }rust
#[neon::export]
async fn fetch_data(url: String) -> String {
// Async Rust (e.g. a network call) integrated with the Node event loop
format!("Fetched: {}", url)
}
// Do synchronous setup on the main thread, then return a future
use std::future::Future;
#[neon::export(async)]
fn process_with_setup(data: String) -> impl Future<Output = String> {
println!("Setup on main thread");
async move {
data.to_uppercase()
}
}Choosing a tool
| You want to... | Use |
|---|---|
| Run CPU-bound work and get a promise | cx.task(...).promise(...) or #[neon::export(task)] |
| Hand JS a promise now, settle later | cx.promise() → Deferred |
| Run async Rust (I/O, network) | #[neon::export] async fn with the futures feature |
| Call back into JS from another thread | cx.channel() + Channel::send, plus Root for callbacks |
One context per execution
Each closure that runs on the main thread receives its own context. Never move a Handle or a FunctionContext into a background thread - move a Root and re-derive the handle inside the Channel::send closure.
See Async Work Pattern for the same flow as an end-to-end diagram.