1. The Problem
Model a sandbox that runs a piece of code and reports a structured result: whether it succeeded, its output, any error, and whether it exceeded a time budget. The runner must never crash the host — a failing or slow job becomes a recorded result, not a thrown exception.
Why this project? Online judges (LeetCode), CI runners, and serverless platforms all run untrusted code and must isolate it. The result contract — success/output/error/timeout — and never letting a job take down the host is the core discipline, separate from the OS-level isolation.
2. How to Think About This
A sandbox needs a uniform result shape: { success, output, error }. Run the job inside a try/catch so an exception becomes success: false with the error captured, never a crash. Model the time budget by checking a reported duration against a limit and returning a timeout result. Real isolation uses OS primitives (separate processes, containers, namespaces) — here we model the contract and the never-crash guarantee, which is the part your code depends on.
3. The Build
// Run `job` (a function) and always return a structured result —
// never throw, no matter what the job does.
export function runSandboxed(job, { timeLimit = 5, reportedTime = 0 } = {}) {
// Model a time budget: a job reporting more than the limit is "killed".
if (reportedTime > timeLimit) {
return { success: false, output: "", error: "Timed out" };
}
try {
const output = job();
return { success: true, output: String(output), error: "" };
} catch (e) {
// A crashing job becomes a recorded failure, not a host crash.
return { success: false, output: "", error: e.message };
}
}
// Demo
console.log(runSandboxed(() => 2 + 2)); // success
console.log(runSandboxed(() => { throw new Error("boom"); })); // caught error
console.log(runSandboxed(() => 1, { timeLimit: 1, reportedTime: 5 })); // timeout
4. Test & Prove
import { runSandboxed } from "./sandbox.mjs";
let pass = 0, fail = 0;
function test(desc, fn) {
try { fn(); console.log(" \u2713", desc); pass++; }
catch (e) { console.log(" \u2717", desc, "\u2014", e.message); fail++; }
}
function assert(cond, msg) { if (!cond) throw new Error(msg || "assertion failed"); }
test("a successful job reports success and output", () => {
const r = runSandboxed(() => 2 + 2);
assert(r.success === true && r.output === "4");
});
test("a throwing job is caught, not propagated", () => {
const r = runSandboxed(() => { throw new Error("boom"); });
assert(r.success === false && r.error === "boom");
});
test("exceeding the time budget yields a timeout", () => {
const r = runSandboxed(() => 1, { timeLimit: 1, reportedTime: 5 });
assert(r.success === false && r.error === "Timed out");
});
test("the host never crashes — always a structured result", () => {
for (const job of [() => 1, () => { throw new Error("x"); }, () => null]) {
const r = runSandboxed(job);
assert("success" in r && "output" in r && "error" in r);
}
});
console.log(`\n${pass} passed, ${fail} failed`);
5. The Interface
node sandbox.mjs
{ success: true, output: '4', error: '' }
{ success: false, output: '', error: 'boom' }
{ success: false, output: '', error: 'Timed out' }6. Run It
node sandbox.mjs
node sandbox.test.mjs
# Real isolation runs the code in a separate process/container, e.g. Node's
# child_process or worker_threads with a kill-on-timeout; the result contract
# above (success/output/error) stays exactly the same.🎯 Try this next
- Run the job in a real Node worker_thread and kill it on a true timeout
- Add a memory limit alongside the time limit
- Capture console.log output from inside the job, not just its return value
- Return the duration so callers can see how close to the limit a job ran
What you practised
The sandbox result contract (success/output/error/timeout), the never-crash-the-host discipline with try/catch, and separating the result model (portable) from OS-level isolation (processes/containers).
Reference: Error Handling · Functions · Objects