Bun vs Node
What behaves differently in JSLab's Bun runtime and in the Browser & Node APIs runtime compared with a plain Node process.
JSLab’s Bun runtime runs your code with the Bun that ships inside JSLab: version 1.4.0 at the time of writing. Help → About JSLab shows the version your copy includes. Bun aims to be Node-compatible, and most code written for Node runs unchanged. These are the differences you’re most likely to notice.
The engine
Bun runs on JavaScriptCore, the engine in Safari, not V8. Anything tied to V8 behaves differently:
- The
v8module is only partly available, and heap snapshots don’t use V8’s format. - Node’s
--inspectflags don’t apply. - Stack trace text is formatted differently. JSLab maps positions back to your code and renders stacks itself, so the output panel looks the same either way.
Built-in modules
node:inspector,node:trace_eventsandnode:replare partial or missing.node:vmworks, with edge-case differences in how contexts behave.worker_threadssupports most APIs; some options are missing.- Native addons built on N-API generally work. Addons that use V8’s C++ API directly don’t.
Module resolution
- Resolution is looser than Node’s, closer to a bundler’s: extensionless imports such as
import "./util"work, andrequireis allowed inside ES modules. - Packages you install from Tools → NPM Packages… are available in every tab without a restart, through
NODE_PATH. - With a working directory set,
<working directory>/node_modulesis searched before JSLab’s own packages.
Environment and files
- JSLab turns off Bun’s own
.envloading and reads a working folder’s.envitself, so the result is the same in every run. See Environment variables for the order the layers apply in. - A working folder’s
.envis read only up to 1 MiB. Anything past that is ignored. - With a working directory,
process.cwd(),__dirnameandimport.meta.dirare that folder, and__filenameandimport.meta.pathare<working directory>/<tab title>.<extension>. Relative imports written as string literals resolve there too. An import whose path is computed at run time, such asimport(name), resolves against the run’s own folder instead. BUN_OPTIONSis removed from every layer of a run’s environment and never reaches your code.NODE_OPTIONSis passed through, but the bundled Bun ignores it, so it has no effect on a run.- A working folder’s
bunfig.tomlis never applied: JSLab launches every run with--config=/dev/null.
Ending a run
process.exit() ends the run once its output is delivered. Code that catches the exit, for example in try/catch, doesn’t keep running: later output is dropped, and timers or servers it starts are stopped.
Installing packages
- Install scripts, such as
postinstall, are blocked unless Allow install scripts is on in the package sheet or Settings → NPM. - npm operations use JSLab’s own
.npmrc, set in Settings → NPM, and never your~/.npmrc. - A git dependency over SSH authenticates through your SSH agent (
SSH_AUTH_SOCK) orGIT_SSH_COMMAND, because npm operations don’t see the~/.sshfolder in your home.
See npm packages.
In the Browser & Node APIs runtime
This runtime is a browser page with Node added, not a Node process, so parts of Node behave differently:
- Only the async APIs work for files and processes.
fs/promises, the callback-stylefsfunctions andchild_process.exec,execFileandspawnwork, bridged out to JSLab. The synchronous versions, such asfs.readFileSync, refuse with a message naming the async alternative and offering to switch the tab to Bun.http,net,tls,dgram,worker_threadsandvmrefuse the same way. processis a snapshot.env,cwd(),platform,argvandversionsare captured when the page loads, andnextTickruns on the microtask queue.process.memoryUsage()isn’t available; use the Bun runtime for it.- Shared built-ins aren’t always the same object.
stream,eventsandbuffereach carry their own copy of what they depend on. Sonew Readable() instanceof EventEmittercan befalse, and aBufferfrom one module may not be recognized by another. Code that branches oninstanceofacross these modules can take the wrong branch. - A command’s environment can differ. A
child_processcall made without an explicitenvinherits JSLab’s own environment, not the layered one a Bun run sees. Passenvyourself when it matters. fetchhas no CORS. It’s routed through JSLab, so cross-origin requests succeed.
In both browser runtimes
alert,confirmandpromptcan’t block. An embedded web view gives them no way to wait for you.alertshows a JSLab notice and returns at once,confirmalways returnsfalse, andpromptalways returnsnull. A console warning says so once per run.import * as nscan show extra keys. Packages are bundled in two halves that join inside the page, and a namespace object can carry the default export’s own keys as well. The values are the same as under Bun; only listing the namespace’s keys, or spreading it into an object, differs.- In the Browser runtime,
fetchis the page’s own, and CORS applies exactly as it would in Safari. - The speaker icon tracks live audio only. See The Web View.