Differences in expected behavior while invoking a `sync` function and an `async` function
Author: cryarchyCreated Aug 13, 2026Updated Aug 13, 2026
Cargo.toml snippet:
...
[dependencies.napi]
version = "3.0.0"
features = ["async", "napi10", "serde-json"]
...Test Code:
import test from 'ava'
import { fnTest, next } from '../index'
test('sync test', (t) => {
return new Promise((resolve, _reject) => {
fnTest(
// synchronous
(v: number) => {
return next({ v: v + 4 })
},
async (v: any) => {
t.deepEqual(v, { v: 20 })
resolve()
return next(null)
},
)
})
})
test('async test', (t) => {
return new Promise((resolve, _reject) => {
fnTest(
// asynchronous
async (v: number) => {
return next({ v: v + 4 })
},
async (v: any) => {
t.deepEqual(v, { v: 20 })
resolve()
return next(null)
},
)
})
})
NAPI code:
#![deny(clippy::all)]
use std::sync::Arc;
use napi::{
bindgen_prelude::*,
threadsafe_function::{ThreadsafeCallContext, ThreadsafeFunction},
};
use napi_derive::napi;
use serde_json::Value as JsonValue;
use tokio::join;
#[napi]
pub fn next(value: Option<Unknown>, env: Env) -> Option<JsonValue> {
value.map(|value| env.from_js_value(value).unwrap())
}
enum HandlerFn {
A(ThreadsafeFunction<u8, Option<JsonValue>, u8, napi::Status, false>),
B(
ThreadsafeFunction<
u8,
napi::bindgen_prelude::Promise<Option<JsonValue>>,
u8,
napi::Status,
false,
>,
),
}
#[napi]
pub fn fn_test(
fxn: Either<Function<u8, Option<JsonValue>>, Function<u8, Promise<Option<JsonValue>>>>,
// #[napi(ts_arg_type = "(v: any) => NextDto")]
eh_fxn: Function<JsonValue, Promise<Option<JsonValue>>>,
) {
let tsfn = match fxn {
Either::A(fxn) => HandlerFn::A(fxn.build_threadsafe_function().build().unwrap()),
Either::B(fxn) => HandlerFn::B(fxn.build_threadsafe_function().build().unwrap()),
};
let eh_tsfn = eh_fxn
.build_threadsafe_function()
.build_callback::<JsonValue, _>(|ctx: ThreadsafeCallContext<JsonValue>| {
Ok(ctx.value.to_owned())
})
.unwrap();
let tsfn = Arc::new(tsfn);
let eh_tsfn = Arc::new(eh_tsfn);
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
let tsfn = tsfn.clone();
let handle = tokio::task::spawn(async move {
// let tsfn_result = tsfn.call_async(16).await.unwrap();
// let next_value = tsfn_result.await.unwrap();
let next_value = match tsfn.as_ref() {
HandlerFn::A(tsfn) => tsfn.call_async(16).await.unwrap(),
HandlerFn::B(tsfn) => {
let tsfn = tsfn.call_async(16).await.unwrap();
tsfn.await.unwrap()
}
};
if let Some(next_value) = &next_value {
let tsfn_result = eh_tsfn.call_async(next_value.to_owned()).await.unwrap();
let _ = tsfn_result.await.unwrap();
}
});
let _ = join!(handle);
});
});
}Error:
Source: napi-rs/napi-rs