Skip to content

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:#fff

JavaScript 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 promisecx.task(...).promise(...) or #[neon::export(task)]
Hand JS a promise now, settle latercx.promise()Deferred
Run async Rust (I/O, network)#[neon::export] async fn with the futures feature
Call back into JS from another threadcx.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.