oven-sh/bun · error · BuildError

Failed to exec

Error message

Failed to exec ${exe}

What it means

scripts/build.ts spawns the just-linked binary with spawnSync and throws this BuildError when the spawn itself errors (child.error set) — the binary could not be launched at all, before any code inside it ran. The original error (ENOENT, EACCES, etc.) is attached as cause.

Solutions

  1. Check the exe path in the message exists and is executable (ls -l)
  2. Rebuild cleanly: remove the build dir and run bun bd again
  3. Fix permissions or move the repo off a filesystem that strips the exec bit
  4. Verify the profile matches the host (no cross-compile profile combined with exec args)
Defensive patterns

Strategy: try-catch

Validate before calling

import { existsSync, accessSync, constants } from "node:fs";
// after the build resolves but before exec
if (!existsSync(exe)) throw new Error(`build output missing: ${exe}`);
accessSync(exe, constants.X_OK); // throws if not executable

Try / catch

try {
  const child = spawnSync(exe, execArgs, { stdio: "inherit" });
  if (child.error) throw new BuildError(`Failed to exec ${exe}`, { cause: child.error });
} catch (e) {
  const code = e.cause?.code;
  if (code === "ENOENT") { /* rebuild then retry once */ }
  else if (code === "EACCES") { /* fix the exec bit / filesystem */ }
  else throw e;
}

Prevention

When it happens

Trigger: ENOENT because result.output.exe points to a path that was not produced or was removed (partial build, pruned build dir), EACCES/EPERM from a missing execute bit, or the OS refusing to run the binary (loader mismatch, quarantine).

Common situations: Build dir cleaned or moved between link and exec; disk full so linking truncated the binary; filesystems that strip the exec bit (network mounts, WSL interop); a cross-compiled binary that does not match the host.

Related errors


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

Appendix: source

Thrown at scripts/build.ts:299

      // it's not obvious ninja finished vs. stalled. This disambiguates.
      // Targets named when explicit so it's clear what was actually built.
      const what = args.ninjaTargets.length > 0 ? ` ${args.ninjaTargets.map(t => nameColor(t)).join(", ")}` : "";
      status(`[build]${what} done`);
      process.exit(0);
    }

    // Exec the built binary. result.output.exe is the linked (unstripped)
    // binary — bun-debug for debug, bun-profile for release. That's the one
    // you want for dev iteration (has symbols + assertions in debug).
    const exe = result.output.exe;
    if (exe === undefined) {
      throw new BuildError("Cannot exec: build mode produced no executable", {
        hint: `mode=${result.cfg.mode} builds artifacts, not a runnable binary. Drop the positional args or use --profile=debug.`,
      });
    }
    const child = spawnSync(exe, args.execArgs, { stdio: "inherit" });
    if (child.error) {
      throw new BuildError(`Failed to exec ${exe}`, { cause: child.error });
    }
    // Signal death: re-raise so our parent sees the same signal (shells
    // show "Segmentation fault" etc. based on this, not exit code).
    if (child.signal) {
      process.kill(process.pid, child.signal);
      return;
    }
    process.exit(child.status ?? 0);
  }
}

/**
 * When an HTTP proxy is configured, cargo's `-Zbuild-std` (release lolhtml)
 * must reach crates.io. Some CI/corporate proxies 403 CONNECT to package
 * registries while direct egress is open. If a proxy is set and crates.io
 * isn't already exempted, probe direct connectivity once: if it works, add
 * crates.io to NO_PROXY so cargo goes direct. If the probe fails (mandatory-
 * egress-proxy topology, firewall drops direct), leave NO_PROXY untouched so

View on GitHub (pinned to 8c5296ac45)