- Node.js
- WebAssembly
- Architecture
- Engineering
How We Run Node.js Code in the Browser
How WebFiddle runs Node.js-style code in a Web Worker with a process and Buffer shim, and the limits: no require, npm packages or fs.
WebFiddle's Node.js sandbox does not run real Node.js. It runs your JavaScript in the browser's own engine inside a Web Worker, with a small shim that provides Node-style globals such as process, Buffer and global. Here is exactly how that works, what it's good for, and where it stops.
Key takeaways
- Your code runs in a dedicated Web Worker, off the page's main thread, using the browser's JavaScript engine. No server executes it.
- Before your code runs, the worker defines a partial
processobject, a minimalBufferclass andglobal. - Your code is wrapped in an
asyncfunction, so top-levelawaitworks. require()throws for every module andimportstatements are a syntax error, so external packages and built-in modules likefs,httpandpathare not available.- The shim is for learning and quick experiments. Anything that needs a file system, sockets or npm packages needs a real Node.js install.
Why run it in the browser at all?
Executing untrusted code on a server means isolating it (containers or VMs), paying for compute and sending every run over the network. Running it on the visitor's device avoids all three. The browser already has a fast JavaScript engine and a built-in way to run code off the main thread: the Web Workers API.
The trade-off is that a browser tab is not an operating system. There is no file system, no raw TCP and no Node.js standard library, so we emulate only a small, honest subset.
The architecture
1. A dedicated Web Worker
When the Node.js sandbox page loads, it starts a worker from /node-worker.js and sends it an INIT message. The worker replies READY, and the editor enables the Run button.
// src/pages/NodeRunner.tsx (simplified)
const worker = new Worker('/node-worker.js');
worker.postMessage({ type: 'INIT' });
// When you press Run
worker.postMessage({ type: 'RUN', payload: { code } });
The worker sends back three kinds of message: OUTPUT (standard output), ERROR (errors and stderr) and DONE. Because your code runs in the worker rather than on the main thread, a long or infinite loop does not freeze the editor. The worker itself stays busy, though, and there is currently no Stop button, so reload the page to recover from an infinite loop.
2. Node-style globals
Browsers and workers don't have process or Buffer, so the worker defines them before running anything. This is the real process shim:
// public/node-worker.js
self.process = {
version: 'v18.16.0 (WebFiddle Shim)',
arch: 'javascript',
platform: 'browser',
env: { NODE_ENV: 'development' },
argv: ['node', 'script.js'],
cwd: () => '/app',
stdout: { write: (chunk) => self.postMessage({ type: 'OUTPUT', payload: chunk }) },
stderr: { write: (chunk) => self.postMessage({ type: 'ERROR', payload: chunk }) },
nextTick: (cb) => setTimeout(cb, 0),
exit: (code) => self.postMessage({ type: 'ERROR', payload: `Process exited with code ${code}` })
};
The version string is a label, not the engine version. Two details behave differently from Node.js:
process.nextTick(cb)is implemented withsetTimeout(cb, 0). In real Node.js, nextTick callbacks run before promise callbacks. Here they run after them, and after any timers scheduled earlier. See the Node.js event loop explained for the real ordering.process.exit()only prints a message. It does not stop the script.
Buffer is a minimal class that wraps a Uint8Array:
// public/node-worker.js
class BufferShim {
constructor(arg) {
if (typeof arg === 'string') {
this.data = new TextEncoder().encode(arg);
} else if (Array.isArray(arg)) {
this.data = new Uint8Array(arg);
} else {
this.data = new Uint8Array(0);
}
}
toString() { return new TextDecoder().decode(this.data); }
static from(arg) { return new BufferShim(arg); }
static isBuffer(arg) { return arg instanceof BufferShim; }
}
self.Buffer = BufferShim;
It supports Buffer.from() with a string or an array of bytes, toString() as UTF-8, and Buffer.isBuffer(). Encodings such as 'base64' or 'hex', alloc(), concat() and indexing into the buffer are not implemented. Finally, global is set to the worker's global scope (self).
3. Console capture
While your script runs, console.log and console.warn send OUTPUT messages and console.error sends ERROR messages. Objects passed to console.log are printed with JSON.stringify(value, null, 2), so functions are omitted, and circular structures cause an error instead of printing the [Circular] marker Node.js would show. Other methods, such as console.table or console.info, are not captured.
4. Top-level await with the Function constructor
The worker wraps your code in an async arrow function and builds it with the Function constructor:
// public/node-worker.js
const userFunction = new Function('require', 'exports', 'module', `return (async () => {
${code}
})();`);
const mockRequire = (name) => {
throw new Error(`Module '${name}' not found. External modules are not supported in this sandbox yet.`);
};
await userFunction(mockRequire, {}, { exports: {} });
Because your code is inside an async function, you can use await at the top level:
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
console.log('Starting...');
await wait(500);
console.log('Half a second later');
The same wrapper explains the module limitations. require is a parameter that always throws, and module and exports are empty placeholder objects. An import statement is not allowed inside a function body, so the browser reports Cannot use import statement outside a module before anything runs.
What works and what doesn't
Works: modern JavaScript syntax, async/await, promises, timers (setTimeout, setInterval), classes, JSON, Map/Set and the web APIs that workers expose, such as fetch, TextEncoder, URL and crypto.getRandomValues. fetch requests are subject to the browser's CORS rules.
Doesn't work: require() and import of any module (built-in or from npm), fs, http, net, child_process, path, setImmediate, EventEmitter, streams, __dirname, and reading environment variables other than the fixed NODE_ENV. If you need packages in the browser, see how to use npm packages in the browser.
Why this approach suits a playground
- No network round trip per run: once the page has loaded, running code doesn't involve our servers.
- Privacy: the code you run stays in your browser. It only leaves your device if you create a share link, which encodes it into the URL.
- Cost: we don't operate execution servers for this sandbox, which helps keep it free.
Frequently asked questions
Does WebFiddle run real Node.js in the browser?
No. The Node.js sandbox runs your JavaScript with the browser's own engine inside a Web Worker, plus a small shim for process, Buffer and global. There is no Node.js binary, libuv or standard library, so APIs like fs, http and require() are not available. It is designed for practising language features.
Can I use npm packages or require modules in the Node.js sandbox?
Not at the moment. The sandbox passes in a require function that throws a "module not found" error for every name, and import statements are rejected because code runs inside a function. Use a local Node.js install for packages, or the React playground, which can load npm packages from a CDN.
Why does top-level await work in the sandbox?
The worker wraps everything you write in an async arrow function before running it. Because your code is inside an async function, the JavaScript engine accepts await at the top level. Real Node.js supports top-level await only in ES modules, such as .mjs files, not in CommonJS scripts.
Is my code sent to a server when I click Run?
No. Your code is posted to a Web Worker in the same browser tab and executed there, and output is sent back to the page as messages. Code only leaves your device if you create a share link, which compresses it into the URL, or if your script itself makes network requests with fetch.
What happens if my code has an infinite loop?
The loop keeps the Web Worker busy, but because the worker runs separately from the page's main thread, the editor stays responsive. The sandbox has no Stop button yet and the worker is not restarted automatically, so reload the page to reset the engine and run code again.
WebFiddle