Execute & Evaluate
Run untrusted JavaScript with evaluate and execute, pass data in with inputs, and read structured results.
Evaluate an expression
evaluate runs one expression and returns its JSON-serializable value.
import { evaluate } from "secure-exec";
// Each call runs in a fresh VM that is disposed when the call finishes.
const sum = await evaluate<number>("1 + 2");
console.log(sum.outcome === "succeeded" ? sum.value : sum.error); // 3
The expression may be a promise, which is awaited. Returning undefined, a
function, a symbol, or a circular value fails instead of silently losing the
value.
Several statements go in a function:
// `evaluate` takes one expression, so several statements go in a function.
const report = await evaluate<{ count: number; max: number }>(`
(() => {
const values = [3, 9, 4];
return { count: values.length, max: Math.max(...values) };
})()
`);
console.log(report.outcome === "succeeded" ? report.value : report.error);
Execute a module
execute runs source as an ES module, so import and top-level await work.
It returns captured output instead of a value.
// `execute` runs a whole ES module for its side effects. Capture output to read
// it back.
const run = await execute(
`
import { platform } from "node:os";
console.log("running on", platform());
`,
{ output: { capture: "all" } },
);
console.log(run.stdout?.trim()); // running on linux
Run a file
executeFile runs a JavaScript file that is already in the VM, by its guest
path. Mount a host directory to run a script in a one-shot call. On a
VM, write the file with vm.filesystem first.
secure-exec/typescript has the same function for TypeScript files.
// `executeFile` runs a file that is already in the VM. With a mount, that works
// in a one-shot call: the script lives on the host and runs inside the VM.
const report = await executeFile("/mnt/data/report.mjs", {
mounts: [hostDirMount("/mnt/data", hostData)],
output: { capture: "all" },
});
console.log(report.stdout?.trim()); // report: hello from the host
A file resolves its imports from its own directory, the same as in Node.js, so a
file under /workspace finds packages installed there.
Pass data in
// `inputs` hands host values to the code as real objects, so data is never
// interpolated into source.
const total = await evaluate<number>(
"inputs.prices.reduce((a, b) => a + b, 0)",
{ inputs: { prices: [5, 10, 27] } },
);
console.log(total.outcome === "succeeded" ? total.value : total.error); // 42
TypeScript
secure-exec/typescript has the same execute, evaluate, and executeFile
functions for TypeScript source. They strip types rather than check them, and a
separate check type-checks without running anything. That is useful for
validating model-generated code before you execute it.
See TypeScript.
Results
Both functions resolve to a result with an outcome.
outcome | Meaning |
|---|---|
succeeded | The code finished. evaluate results carry value. |
failed | The code threw or exited non-zero. |
timed_out | timeoutMs elapsed. |
cancelled | The signal you passed was aborted. |
// Guest errors are returned, not thrown. Capture stderr to see the stack.
const failed = await evaluate(`JSON.parse("not json")`, {
output: { capture: "stderr" },
});
if (failed.outcome !== "succeeded") {
console.log(failed.outcome, failed.stderr?.split("\n")[0]); // failed SyntaxError: ...
}
Guest failures are returned
Anything the guest code does wrong comes back as a result, never as an exception, so untrusted code can never crash your process.
// What the guest code does wrong is returned as a result, never thrown.
const failed = await evaluate(`null.length`, { output: { capture: "stderr" } });
console.log(failed.outcome); // failed
if (failed.outcome !== "succeeded") {
console.log(failed.error.code); // execution_failed
console.log(failed.stderr?.split("\n")[0]); // TypeError: Cannot read properties of null ...
}
outcome | error.code | Cause |
|---|---|---|
failed | execution_failed | The code threw, or exited non-zero |
failed | evaluation_serialization_failed | evaluate produced a value that is not JSON |
timed_out | timeoutMs elapsed | |
cancelled | The signal you passed was aborted |
The guest’s own message and stack trace arrive on stderr. Capture it with
output: { capture: "stderr" } when you want to show it to a user or a model.
Host mistakes are thrown
A call rejects only when the host asked for something that cannot run.
// What the host asks for wrong is thrown, as a typed error with a stable code.
const vm = await createVm();
const context = await vm.createContext();
const slow = context.evaluate(
"new Promise((resolve) => setTimeout(resolve, 500))",
);
try {
await context.evaluate("1 + 1"); // The context is still busy with `slow`.
} catch (error) {
if (error instanceof SidecarRejectedError) {
console.log(error.detail.code); // execution_busy
}
}
await slow;
await vm.dispose();
| Error | When |
|---|---|
SidecarRejectedError | The sidecar refused the request. error.detail.code says why |
SidecarProcessExited | The sidecar process died |
SidecarSilenceTimeout | The sidecar stopped responding |
KernelError | A host-side filesystem or process operation failed, with a POSIX-style code |
TypeError / validation error | An option is missing or malformed |
Common detail.code values on SidecarRejectedError:
| Code | Meaning |
|---|---|
execution_busy | The context is already running a call |
context_not_found | The context was disposed, or its VM was |
Errors that hit a limit name the limit and the option that raises it. See Resource Limits.
Options
| Option | Purpose |
|---|---|
inputs | JSON values the code reads from inputs |
timeoutMs | Stop the code after this long. Set it for any code you did not write |
signal | An AbortSignal that cancels the call |
output, onStdout, onStderr | Capture or stream output |
env, cwd, args, stdin | The process environment the code sees. The working directory defaults to /workspace |
filePath | The path the code reports in stack traces and resolves imports from |
The top-level functions also take VM options,
such as permissions, limits, and mounts, because each call creates its own
VM. To keep anything between calls, create a VM and
call the same methods on vm.javascript.