Summary
interopDefault in src/utils.ts:151-156 wraps fall-through methods with Function.prototype.bind to preserve this for the underlying default export. When the property accessed is Symbol.asyncDispose / Symbol.dispose and the consumer uses native await using / using (Node 24+, no jiti transpilation of the calling file), V8 rejects the bound function returned through the proxy get trap with:
TypeError: Symbol(Symbol.asyncDispose) is not a function
This does not surface when the calling file is itself transpiled by jiti, because Babel rewrites await using to a DisposableStack helper that calls the bound function directly.
Minimal repro (no jiti)
// repro.mjs — Node 24+
const def = { [Symbol.asyncDispose]: async () => console.log("disposed!") };
const proxy = new Proxy({ default: def }, {
get(t, prop) {
if (prop === Symbol.asyncDispose) return def[Symbol.asyncDispose].bind(def);
},
});
console.log("manual lookup ok:", proxy[Symbol.asyncDispose]); // works
{
await using x = proxy; // throws: Symbol(Symbol.asyncDispose) is not a function
}
Replacing .bind(def) with an arrow wrapper makes V8 accept it:
get(t, prop) {
if (prop === Symbol.asyncDispose) {
const fn = def[Symbol.asyncDispose];
return (...args) => fn.apply(def, args);
}
}
End-to-end repro (with jiti)
// db.ts
class DB {
async query(sql: string) { console.log(`[db.query] ${sql}`); }
async [Symbol.asyncDispose]() { console.log("[db] disposed"); }
}
export default new DB();
// run.ts
import { createJiti } from "jiti";
const jiti = createJiti(import.meta.url);
await using db = await jiti.import("./db.ts");
await db.query("SELECT 1");
node run.ts (Node 24.10 with --experimental-strip-types / native TS) → throws TypeError: Symbol(Symbol.asyncDispose) is not a function
node lib/jiti-cli.mjs run.ts → works (Babel transpiles await using)
Workaround
Bypass the proxy by reading .default directly, or use the default: true option:
await using db = (await jiti.import("./db.ts")).default;
// or
await using db = await jiti.import("./db.ts", { default: true });
Suggested fix
In interopDefault, replace value.bind(def) with an arrow wrapper for the fall-through case:
- if (typeof value === "function") {
- value = value.bind(def);
- }
+ if (typeof value === "function") {
+ const fn = value;
+ value = (...args: any[]) => fn.apply(def, args);
+ }
Environment
- Node v24.10.0 (also reproduces on v25.5.0)
- jiti
main (post-2.7.0)
Summary
interopDefaultinsrc/utils.ts:151-156wraps fall-through methods withFunction.prototype.bindto preservethisfor the underlying default export. When the property accessed isSymbol.asyncDispose/Symbol.disposeand the consumer uses nativeawait using/using(Node 24+, no jiti transpilation of the calling file), V8 rejects the bound function returned through the proxygettrap with:This does not surface when the calling file is itself transpiled by jiti, because Babel rewrites
await usingto aDisposableStackhelper that calls the bound function directly.Minimal repro (no jiti)
Replacing
.bind(def)with an arrow wrapper makes V8 accept it:End-to-end repro (with jiti)
node run.ts(Node 24.10 with--experimental-strip-types/ native TS) → throwsTypeError: Symbol(Symbol.asyncDispose) is not a functionnode lib/jiti-cli.mjs run.ts→ works (Babel transpilesawait using)Workaround
Bypass the proxy by reading
.defaultdirectly, or use thedefault: trueoption:Suggested fix
In
interopDefault, replacevalue.bind(def)with an arrow wrapper for the fall-through case:Environment
main(post-2.7.0)