Skip to content

Bug: interopDefault proxy returns bound functions, breaking native await using (Node 24+) #437

Description

@pi0

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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions