JSLab is a JavaScript & TypeScript scratchpad for macOS, and every run in a Bun tab is a separate Bun process. Bun aims to be Node-compatible, and most Node code runs unchanged. But "most" is where a tool has to make decisions. Here are seven differences, what JSLab does about each, and what they mean if you use Bun yourself.

1. It's JavaScriptCore, not V8

Bun runs on JavaScriptCore, Safari's engine. Anything tied to V8 behaves differently: the v8 module is only partly there, heap snapshots aren't V8's format, --inspect flags don't apply, and stack trace text is formatted differently.

That last one matters if you parse stack traces. JSLab doesn't parse the text: it maps positions back to your source and renders stacks itself, so the output panel looks the same whichever engine produced the error.

2. Module resolution is looser

Bun resolves more like a bundler than like Node:

import { slugify } from "./util"; // no extension: fine in Bun const fs = require("node:fs"); // require inside an ES module: also fine 

Code that works in Bun can fail in strict Node ESM. If you're writing for both, keep the extensions.

3. Bun loads .env on its own

By default Bun reads .env files from the current folder. Handy in a project, surprising in a tool. JSLab turns that off and reads the working folder's .env itself, so the result is the same in every run, and it layers it with your shell and its own Environment Variables sheet in a fixed order.

4. bunfig.toml can change everything

A bunfig.toml in the folder can change how Bun resolves and runs code. JSLab launches every run with --config=/dev/null, so a folder's bunfig.toml is never applied. For the same reason, BUN_OPTIONS is stripped from the run's environment. And NODE_OPTIONS? Passed through, but Bun ignores it.

5. process.exit() and output

A snippet that logs and then calls process.exit() should still show its logs. In JSLab, process.exit() ends the run once its output is delivered. And code that catches the exit (in a try/catch, say) doesn't keep running: later output is dropped, and its timers and servers are stopped.

6. Packages without a restart

JSLab installs npm packages into one shared project, and every Bun run finds them through NODE_PATH. That's why a package you add in one tab is importable in every other tab straight away. With a working directory set, that folder's node_modules is searched first, so you get the project's versions.

7. The Bun is pinned

JSLab ships its own Bun (1.4.0 at the time of writing; Help → About JSLab shows yours). Your snippets don't depend on whether you have Bun installed, or which version is on your PATH.

What this adds up to

Each of these comes from the same rule: a run should behave the same every time, whatever folder or shell it starts from. If you build tools on Bun, it's worth applying that rule to the same seven places.

A JSLab tab on Bun: "document" in globalThis returns false, "Bun" in globalThis returns true, process.versions.bun returns 1.4.0, and readdir(".") counts 16 files, each value tagged to its line.

The full list, including the Browser & Node APIs runtime, is on one docs page.

JSLab launches Nov 15. Bun vs Node: https://js-lab.app/docs/bun-vs-node