I ran into several unexpected behaviors while experimenting with jco async imports.
Some of them may be related, so I’m listing them below.
Current generated code:
foo?._isHostProvided = true;
...
await foo(arg);
However, since foo is a method on the resource instance, it seems the host-provided check should instead refer to the constructor. Something like:
let hostProvided = false;
hostProvided = rsc0.constructor._isHostProvided;
...
await rsc0.foo(arg);
It looks like the generated code treats the method as a free function instead of a method on the resource instance.
--map option, _isHostProvided is not initialized.In contrast, when using --instantiation, _isHostProvided is initialized correctly.
Even if issue (1) is fixed, _isHostProvided remains undefined for JS class.
As a result, the following branch is never taken:
if (hostProvided) {
task.resolve([ret]);
endCurrentTask(0, task.id());
return task.completionPromise();
}
result type cause a runtime error.In _lowerFlatVariant, the following code is generated:
if (!variant.discriminant) {
throw new Error(`missing/invalid discriminant for variant [${variant}]`);
}
However, when the value is ok, variant.discriminant === 0.
Since 0 is falsy in JavaScript, this condition incorrectly treats the value as invalid and throws the error.
It seems the check should instead verify that the discriminant is undefined or null, e.g.:
if ((variant.discriminant === undefined) || (variant.discriminant === null)) {
throw new Error(`missing/invalid discriminant for variant [${variant}]`);
}
For example:
async fn invoke() -> u32 {
let v = my_intf::foo().await;
my_intf::bar(v).await // error occurs here
}
When calling a function exported from Wasm, the first call (task) transitions to AsyncSubtask.State.STARTED due to the following code:
if (currentSubtask && currentSubtask.isNotStarted()) {
currentSubtask.onStart();
}
However, for the second call, onStart is not invoked for the subtask, so it proceeds with #state == AsyncSubtask.State.STARTING.
As a result, the following check in AsyncSubtask.onResolved throws an error:
if (this.#state !== AsyncSubtask.State.STARTED) {
throw new Error(
"cancelled subtask must have been started before cancellation",
);
}
As a workaround, calling subtask.onStart() inside the setTimeout handler registered by _lowerImport allows the call sequence to complete successfully.
This suggests that the subtask may still be in the STARTING state when the trampoline is invoked.
setTimeout(async () => {
...
if (subtask.state() !== AsyncSubtask.State.STARTED) {
subtask.onStart();
}
exportFn.apply(null, params);
...
});
This appears to be related to https://github.com/bytecodealliance/jco/issues/1272.
Hey @ktz_alias thanks for the detailed bug report! Would you mind filing this at https://github.com/bytecodealliance/jco/issues ?
Also, if you could include which version of Jco you're using that would be great! I think the _isHostProvided issues persist but I'm not sure the others aren't already solved.
@Victor Adossi Thanks!
I’ve already filed a related issue (#1272). I’ll update it with the additional findings and workaround.
For the cases that seem to be separate issues (although they occur along the same flow), I’ll open new issues and link them for clarity.
Followed up on GitHub with details and workarounds:
Thanks, that's great! Thanks for splitting them up as well, will try and get through them ASAP!
Working on fixing the struct lifting now, some work that was already underway to finish the async implementation is what you've run into, I think
Last updated: Mar 23 2026 at 16:19 UTC