Skip to content

Invoking JS function from native code twice #1439

Description

@skjolber

What happened?

Hi,

we are writing a plugin for handling travel rights in public transport. As part of this work, we manage one or more key-pairs which are used to authenticate against the backend. The key-pair has a rather complex lifecycle. Master state is determined by the server.

As part of this work we are providing apps with a plugin method which allows them to easily interact with the backend:

const result = await clientRef.current?.remoteClientCallHandler<string>({
	tokenContextId: DEFAULT_TOKEN_CONTEXT_ID,
	tokenEncodingRequest,
	fn: async (params) => {
	    // custom work there
	}
})

where fn is

export type TravellerClientCall<R> = (input: RemoteClientCallInput) => Promise<R>

and params is some values we need the client to forward in order to be authenticated, i.e.

export type RemoteClientCallInput = {
    tokenId: string
    secureContainerToken: string
    attestation: Attestation | undefined
    correlationId: string
}

So the async JS-function fn is essentially passed to native code, then invoked from an executor thread in Android.

In native (Java) code, invoking fn returns a Promise<Promise<MetadataResult>> where MetadataResult accounts for various expected throwable errors. This works as expected, i.e. first waiting for Promise<MetadataResult> and then MetadataResult.

The problem arises when we want to invoke fn twice from a single TravellerClientCall when the first fn-invocation throws an error, the second fn-invocation hangs forever. To be more precise, awaiting MetadataResult hangs on the second invocation of fn.

However the fn JS code actually executes a second time (returns a value, does not throw). And the hang is not happening if we insert an await sleep(1 millisecond) in the code path which runs on the second invocation. So this indicates some kind of race condition / signalling / threading problem. To be clear, in the unit test, the fn returns almost immidiately. Everything is async.

So what we are wondering is whether this approach is expected to work and/or any obious solution we might have missed, and how to continue from here - I have already tried everything I can think of.

Reproduceable Code

-

Relevant log output

A thread dump indicates 


2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP      com.abtmobileclientsdkexample        W  "default-idleing-pool-thread-1" state=WAITING
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP      com.abtmobileclientsdkexample        W      at jdk.internal.misc.Unsafe.park(Native Method)
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP      com.abtmobileclientsdkexample        W      at java.util.concurrent.locks.LockSupport.park(LockSupport.java:226)
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP      com.abtmobileclientsdkexample        W      at java.util.concurrent.CompletableFuture$Signaller.block(CompletableFuture.java:1840)
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP      com.abtmobileclientsdkexample        W      at java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3797)
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP      com.abtmobileclientsdkexample        W      at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3738)
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP      com.abtmobileclientsdkexample        W      at java.util.concurrent.CompletableFuture.waitingGet(CompletableFuture.java:1874)
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP      com.abtmobileclientsdkexample        W      at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2048)
2026-07-15 18:20:43.183 24905-24925 HARNESS_THREADDUMP      com.abtmobileclientsdkexample        W      at com.margelo.nitro.abt.JavaHybridAbtMobileClientSDK.lambda$remoteClientCallHandlerImpl$7(JavaHybridAbtMobileClientSDK.java:640)

Device

Android 17 Emulator

Nitro Modules Version

0.36.1

Nitrogen Version

0.36.1

Can you reproduce this issue in the Nitro Example app here?

I didn't try (⚠️ your issue might get ignored & closed if you don't try this)

Additional information

Metadata

Metadata

Assignees

No one assigned

    Labels

    nitro-coreIssue is related to the Nitro Modules core runtime/C++ codebase

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions