What a runtime is and why code does not work everywhere the same way
There is one thing that confuses beginners often: you write JavaScript, it works perfectly in the browser — but when you try to run it on a server, nothing happens. Or vice versa: a script runs in Node.js but crashes in the browser complaining about a missing function.
The problem is not the code. The problem is where that code runs. And that “where” is the runtime (execution environment).
Simple analogy: JavaScript is a language. And the runtime is the country where you speak it. The grammar is the same, but the laws, culture, and available services are different.
The problem: why “it works for me” is not the same thing
JavaScript was originally created for browsers. It was a language that lived inside web pages, could manipulate the DOM, react to clicks, and change colors. But it couldn’t read files, couldn’t talk to the network directly, and had no access to the filesystem.
Then Node.js came along — and JavaScript went beyond the browser. On the server it gained access to files, networking, and databases — but it lost the DOM. There is no window or document here. It’s a different world.
Then others arrived: Bun, Deno, Cloudflare Workers — each with their own quirks.
The same JavaScript syntax works in all of them. But the set of tools available to the code is different. And that is where the errors happen.
How this works in plain language
A runtime is three things:
1. The JavaScript engine
This is what actually understands and runs your JavaScript code. Most modern runtimes use one of three main engines:
- V8 (Google) — used in Chrome, Node.js, Bun. Fast, well-optimized.
- JavaScriptCore (Apple) — used in Safari. Runs on all Apple devices.
- SpiderMonkey (Mozilla) — used in Firefox. Open and independent.
The engine parses (reads) your code, transforms it into machine instructions, optimizes, and executes it.
2. Global objects and APIs
This is where runtimes start to differ:
In the browser:
window // global browser object
document // DOM — the entire page
fetch() // network requests
setTimeout() // timers
localStorage // client-side data storage
In Node.js:
global // global object (instead of window)
require() // file imports
fs // filesystem access
http // server creation
process // process information (PID, environment variables)
In Bun:
Bun.serve() // built-in web server
Bun.file() // convenient file handling
fetch() // built-in, no extra dependencies needed
Core JavaScript (variables, functions, classes) works the same everywhere. But the APIs do not.
3. Module system and dependencies
Each runtime handles the question of “how do I import code from another file?” differently:
- Node.js (early versions) — CommonJS:
const x = require('./module') - Node.js (modern versions) and most current runtimes — ES Modules:
import x from './module' - Bun — supports both approaches without extra setup
- Deno — uses direct URLs for imports:
import x from 'https://example.com/mod.ts'
Why this matters in practice
1. Picking the right tool for the job
If you’re building a server — Node.js or Bun will work. If you’re working with frontend code — the browser itself is the runtime. If you’re running functions in the cloud — maybe Cloudflare Workers or AWS Lambda.
There is no “correct” runtime. There is the right one for your specific task.
2. Understanding why code fails
When you understand the difference between runtimes, the nature of many errors becomes obvious:
ReferenceError: window is not defined— you’re trying to use a browser API in Node.js or Bun.require is not defined— you’re running in ES Modules mode, whererequire()doesn’t exist.fs is not defined— you’re trying to access the filesystem from a browser (which isn’t possible without special APIs).
3. Performance
Different runtimes have different performance characteristics. Node.js is mature with the largest ecosystem. Bun has significantly faster startup and less memory usage but is younger. Deno has built-in security and TypeScript out of the box.
For a small script, performance doesn’t matter much. For a high-load server — it might be critical.
A practical example: the same code, different runtimes
Here’s a simple file:
// hello.js
console.log("Hello!");
console.log(typeof window); // browser API?
console.log(typeof process); // Node.js API?
console.log(typeof Bun); // Bun API?
Running in the browser (via devtools console):
Hello!
object // window — browser object
undefined // process — not available
undefined // Bun — not available
Running in Node.js:
Hello!
undefined // window — not available
object // process — available
undefined // Bun — not available
Running in Bun:
Hello!
undefined // window — not available
object // process — available (Bun emulates Node.js APIs)
object // Bun — available
Same code, three different results. Not because of a bug — because of different environments.
How to pick a runtime for your project
The question is not which runtime is “best.” The question is what you need:
Node.js — if:
- you need the largest ecosystem of packages (npm);
- you know that community support and stability matter more than raw speed;
- your team is already familiar with Node.js.
This is the default choice. It is not always the fastest, but it is always the safest.
Bun — if:
- you need maximum startup and execution speed;
- you want built-in APIs (server, files, testing) without installing extra packages;
- your project is new and you are not limited by old libraries that only work in Node.js.
Bun is gaining popularity but it is younger. Not everything on npm works with it without issues.
Deno — if:
- you want TypeScript out of the box (no setup needed);
- security by default matters (Deno doesn’t grant network or filesystem access without explicit permissions);
- you like the idea of importing without a package manager.
Deno is conceptually interesting but its ecosystem is smaller.
Common mistakes
1. Assuming every npm package works everywhere
Not every npm package is universal. If a package relies on fs or process, it won’t work in the browser or in Cloudflare Workers. Always check the package documentation.
2. Confusing the language with the runtime
JavaScript is not Node.js. Those are two different things. JavaScript is the language (syntax, operators, functions). Node.js is one of the environments where that language runs, and it adds its own APIs.
3. Ignoring versions
Node.js 14, 18, 20 — these are different runtimes with different capabilities. What works in Node.js 20 might not work in 14. Always check the minimum supported version.
4. Using the wrong runtime for the task
The browser is not suitable for server-side logic. Node.js is not suitable for frontend rendering (without extra tooling). Cloudflare Workers are not suitable for long-running tasks.
Conclusion / action plan
The runtime determines which tools your code has access to. Understanding that difference is the key to making “it works” a reliable outcome rather than a surprise.
Here is what to do next:
- Figure out which runtime you are currently using (browser, Node.js, or something else).
- Run a simple JS file in two different runtimes and compare the output.
- Check whether your dependencies actually work in your chosen runtime.
- If the project is new — consider alternatives (Bun, Deno) and benchmark them.
- Add a runtime version check to your build scripts to avoid surprises.
Once you stop thinking “JavaScript is the same everywhere” and start thinking “which runtime do I need” — your understanding jumps noticeably.