Output Capture
exec() runs guest code and resolves with its captured output once the guest process exits.
stdoutandstderrare buffered as separate strings, so output on one channel never bleeds into the other.exitCodeis the guest process’s exit status.- Buffering into strings bounds host memory: each channel is held as one string, not an unbounded backlog of live chunks.
Capturing output
Section titled “Capturing output”import { NodeRuntime } from "secure-exec";
const rt = await NodeRuntime.create();
try { const { stdout, stderr, exitCode } = await rt.exec(` console.log("hello from the sandbox"); console.error("oops from the sandbox"); process.exit(3); `);
console.log("exitCode:", exitCode); console.log("stdout:", JSON.stringify(stdout)); console.log("stderr:", JSON.stringify(stderr));} finally { await rt.dispose();}Prints:
exitCode: 3stdout: "hello from the sandbox\n"stderr: "oops from the sandbox\n"process.exit(3) is why exitCode is 3.
run() adds one more field. It resolves to the same stdout, stderr, and exitCode, plus a decoded value the guest hands back by calling globalThis.__return(value):
const { value, exitCode } = await rt.run(` const sum = [1, 2, 3].reduce((a, b) => a + b, 0); globalThis.__return({ sum });`);
console.log(exitCode, value); // 0 { sum: 6 }value is the JSON-decoded argument passed to __return(). It is only populated when exitCode === 0 and the guest actually called __return(); otherwise it is undefined, while stdout, stderr, and exitCode are still captured.
See the TypeScript SDK reference for the full result and option shapes.
Streaming output
Section titled “Streaming output”Pass onStdout / onStderr to observe output incrementally as it is produced. The full strings are still returned when the run ends.
- Each callback receives a
Uint8Arraychunk. Decode text with aTextDecoder. - Available on both
exec()andrun(), and onspawn()viaNodeRuntimeSpawnOptions.
const decoder = new TextDecoder();
const { stdout, exitCode } = await rt.exec( ` for (let i = 0; i < 3; i++) console.log("tick", i); `, { onStdout: (chunk) => process.stdout.write(decoder.decode(chunk)), onStderr: (chunk) => process.stderr.write(decoder.decode(chunk)), },);// stdout still holds the complete buffered output after the run.Feeding input and cancelling
Section titled “Feeding input and cancelling”exec(), run(), and spawn() take the same options object. Beyond the streaming hooks above, the common ones are stdin to pipe input in, signal to cancel, plus env, cwd, and timeout. The TypeScript SDK reference lists them all.
Feed input and read everything back as buffered strings:
const { stdout } = await rt.exec( `process.stdin.pipe(process.stdout);`, { stdin: "echo me back\n" },);// stdout === "echo me back\n"Cancel an in-flight run with signal; the guest process is killed inside the VM and the call rejects with the abort reason:
const controller = new AbortController();const pending = rt.exec("while (true) {}", { signal: controller.signal });controller.abort();await pending.catch((err) => console.log(err.name)); // "AbortError"Common patterns
Section titled “Common patterns”Use exitCode to detect failure, with stderr for the diagnostic:
const { stderr, exitCode } = await rt.exec(code);if (exitCode !== 0) throw new Error(`guest exited ${exitCode}: ${stderr}`);For long-running guests that never exit (dev servers), use spawn() instead: it returns a live NodeRuntimeProcess handle with the same onStdout/onStderr streaming, plus writeStdin, kill, and wait().