#3455·napi-rs

Differences in expected behavior while invoking a `sync` function and an `async` function

Author: cryarchyCreated Aug 13, 2026Updated Aug 13, 2026

Cargo.toml snippet:

toml
...
[dependencies.napi]
version = "3.0.0"
features = ["async", "napi10", "serde-json"]
...

Test Code:

typescript
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:

rust
#![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:

Image