Appearance
Workflow: Async Work Pattern
Fresh 🌱The canonical pattern for doing real work without blocking the event loop: take the request on the main thread, hand JavaScript a promise immediately, do the heavy work on another thread, then settle the promise back on the main thread.
The flow
sequenceDiagram
participant JS as JavaScript
participant MAIN as Main thread (Rust)
participant CH as Channel
participant WORK as Worker / spawned thread
JS->>MAIN: call asyncFn(args)
MAIN->>MAIN: cx.promise() -> (deferred, promise)
MAIN-->>JS: return promise (pending)
MAIN->>WORK: spawn work (move Roots + deferred + channel)
Note over WORK: Pure Rust work,
no JS access here
WORK->>CH: deferred.settle_with(&channel, closure)
CH->>MAIN: schedule closure on main thread
MAIN->>MAIN: build JS result value
MAIN-->>JS: promise resolvesThe shape in code
rust
fn async_operation(mut cx: FunctionContext) -> JsResult<JsPromise> {
let input: String = cx.argument::<JsString>(0)?.value(&mut cx);
let channel = cx.channel(); // bridge back to main thread
let (deferred, promise) = cx.promise(); // give JS a promise now
std::thread::spawn(move || {
let result = input.to_uppercase(); // heavy work, off the event loop
deferred.settle_with(&channel, move |mut cx| {
Ok(cx.string(result)) // build JS value on main thread
});
});
Ok(promise) // returned immediately, still pending
}The three rules this pattern enforces
- Return fast. The function returns the pending promise right away, so the event loop is never blocked.
- No JS off the main thread. The spawned thread does only pure Rust work. Any JavaScript value creation happens inside the
Channelclosure, which runs on the main thread. - Cross the boundary with the right carriers. Move a
Deferredand (if you need callbacks/objects) aRootinto the thread. Use theChannelto schedule the JS-touching closure home.
Two shortcuts
| If your work is... | Use the shortcut |
|---|---|
| CPU-bound, fits the worker pool | cx.task(|| heavy()).promise(|cx, r| ...) or #[neon::export(task)] |
| Async Rust I/O (network, etc.) | #[neon::export] async fn with the futures feature |
Both wrap this exact pattern so you do not manage the channel and deferred by hand. Full details in Async Tasks, Promises & Channels.
The most common bug
Moving a Handle or FunctionContext into the spawned thread will not compile (and must not) - they are not Send. Move a Root and re-derive the handle inside the Channel closure with root.into_inner(&mut cx).