oven-sh/bun · error · Error

$$${name} takes two string arguments, but got '$$${name}${in

Error message

$$${name} takes two string arguments, but got '$$${name}${inner.result}'

What it means

$rust(struct, fn) and $cpp(struct, fn) lazily bind an existing native function at bundle time and require exactly two string literal arguments. This error fires when the argument list parsed successfully but the shape is wrong — wrong count, non-string arguments, or a number in a string slot.

Source

Thrown at src/codegen/replacements.ts:260

          inner.result
            .slice(1, -1)
            .replaceAll("'", '"')
            .replace(/,[\s\n]*$/s, "") +
          "]";
        args = JSON.parse(str);
      } catch {
        throw new Error(`Call is not known at bundle-time: '$${name}${inner.result}'`);
      }
      if (
        args.length != (is_create_fn ? 3 : 2) ||
        typeof args[0] !== "string" ||
        typeof args[1] !== "string" ||
        (is_create_fn && typeof args[2] !== "number")
      ) {
        if (is_create_fn) {
          throw new Error(`$${name} takes three arguments, but got '$${name}${inner.result}'`);
        } else {
          throw new Error(`$${name} takes two string arguments, but got '$${name}${inner.result}'`);
        }
      }

      const id = registerNativeCall(kind, args[0], args[1], is_create_fn ? args[2] : null);

      return [slice.slice(0, match.index) + "__intrinsic__lazy(" + id + ")", inner.rest, true];
    } else if (name === "isPromiseFulfilled" || name === "isPromiseRejected" || name === "isPromisePending") {
      const inner = sliceSourceCode(rest, true);
      // JSC::JSPromise::Status: Pending = 0, Fulfilled = 1, Rejected = 2.
      const status = name === "isPromisePending" ? 0 : name === "isPromiseFulfilled" ? 1 : 2;
      let args;
      if (debug) {
        // use a property on @lazy as a temporary holder for the expression. only in debug!
        args = `($assert(__intrinsic__isPromise(__intrinsic__lazy.temp=${inner.result.slice(0, -1)}))),__intrinsic__peekPromiseStatus(__intrinsic__lazy.temp) === (__intrinsic__lazy.temp = undefined, ${status}))`;
      } else {
        args = `(__intrinsic__peekPromiseStatus${inner.result} === ${status})`;
      }
      return [slice.slice(0, match.index) + args, inner.rest, true];

View on GitHub (pinned to 8c5296ac45)

Solutions

  1. Pass exactly two string literals: $rust("StructName", "functionName").
  2. Use $newRustFunction/$newCppFunction if the binding needs a third length argument.
  3. Check for a stray trailing comma or removed argument after edits.

Example fix

// before
const fn = $rust("MyStruct");

// after
const fn = $rust("MyStruct", "myFn");
Defensive patterns

Strategy: validation

Validate before calling

const rustRe = /\$(rust|cpp)\((.*?)\)/g;
for (const m of src.matchAll(rustRe)) {
  const args = m[2].split(",").map(s => s.trim());
  if (args.length !== 2 || !args.every(a => /^(['"]).*\1$/.test(a))) {
    throw new Error(`$${m[1]} needs exactly two string literals: ${m[0]}`);
  }
}

Prevention

When it happens

Trigger: Calling $rust("Struct") with one argument, passing a number as one of the names, or copying a $newRustFunction three-arg call into a $rust site.

Common situations: Switching between $rust and $newRustFunction forms while editing builtin FFI bindings; accidentally deleting one argument.

Related errors


AI-assisted analysis of oven-sh/bun@8c5296ac45 (2026-08-16). Data as JSON: /api/errors/0119f32627d2a50d. Report an issue: GitHub.