1 When a hello isn’t hello
JavaScript feels easy to begin using because much of its complexity is hidden: there is no obvious compilation step, memory management is automatic, and asynchronous behavior often appears to “just work.” The chapter argues that this simplicity can be misleading. Real JavaScript programs depend not only on the language itself, but also on the engine and host runtime that parse, execute, schedule, and coordinate code with the outside world.
A central example shows identical asynchronous code producing different output depending on whether it runs in Node.js as CommonJS, Node.js as an ECMAScript module, Deno, or Bun. The differences come from runtime-specific scheduling rules involving queues such as microtasks, next ticks, timers, and immediates. The lesson is that developers should be cautious about relying on precise ordering of asynchronous operations, because host runtimes make different choices based on their design goals and histories.
The chapter frames the book as an “owner’s manual” for understanding how JavaScript really works when problems arise. It introduces the distinction between language-defined behavior, which should be consistent everywhere; implementation-defined behavior, which depends on the JavaScript engine; and host-defined behavior, which can vary widely by runtime. This foundation prepares readers to examine deeper topics such as types, promises, streams, cryptography, and module systems with a focus on how real production behavior emerges from the interaction between specification, engine, and host.
An illustration of the phases of the Node.js Event Loop from the project documentation (https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick). At the start of each iteration Node.js will first execute timers, then move on to calling pending callbacks, preparing for I/O, polling the operating system for I/O, then performing various checks and cleanup operations before starting over again at the top with timers.
Summary
- Identical JavaScript code can yield different results in Node.js, Deno, and Bun due to differences in runtime implementation choices
- The event loop controls execution order, with each runtime implementing different scheduling priorities
- Language specifications define what must be consistent; implementations and hosts define what can vary
- Understanding the distinction between language, implementation, and host behaviors prevents unexpected bugs
FAQ
Why does the chapter say “a hello isn’t hello”?
The chapter uses a small asynchronous JavaScript puzzle that appears to print Hello. In Node.js CommonJS, it does print Hello, but the same or nearly same code can produce different output in Node.js ECMAScript modules, Deno, and Bun. The point is that JavaScript behavior is influenced not only by the language, but also by the runtime, module system, queues, and scheduling rules.
What output does the first puzzle produce in Node.js CommonJS?
When run in Node.js version 24 as a CommonJS script, the puzzle prints:
HelloThis happens because Node.js drains the nextTick queue before the microtask queue, then starts the event loop, processes the timeout, and finally drains the immediates queue.
Why does the output not match the order of the statements in the code?
The statements schedule asynchronous tasks rather than executing their callbacks immediately. APIs such as queueMicrotask(), process.nextTick(), setTimeout(), promise continuations, and setImmediate() place callbacks into different queues. The runtime decides when each queue is processed, so the printed letters appear in runtime-defined execution order rather than source-code order.
Why does changing a Node.js file from .js to .mjs change the result?
Changing the file extension can cause Node.js to run the code as an ECMAScript module instead of CommonJS. That changes how Node.js evaluates the script, drains queues, and runs the event loop. In the chapter’s example, identical code produces Hello as CommonJS but produces:
elHo
las an ECMAScript module.
Why do Node.js, Deno, and Bun produce different results for similar code?
Each runtime has its own host-defined scheduling behavior and APIs. Node.js, Deno, and Bun were built with different goals and have different event loop and compatibility decisions. Deno originally focused more on browser compatibility and added Node.js compatibility later. Bun uses JavaScriptCore rather than V8. These differences affect the ordering of timers, microtasks, next ticks, and immediates.
What are host runtimes?
Host runtimes are applications that embed a JavaScript engine and provide the environment in which JavaScript code runs. Examples include web browsers, Node.js, Deno, Bun, and Cloudflare Workers. The host runtime controls access to external resources such as networking, file systems, timers, and operating-system events, and it decides how JavaScript execution is scheduled.
What is the difference between a JavaScript engine and a host runtime?
The JavaScript engine parses, compiles, interprets, and executes JavaScript code. Examples include V8, JavaScriptCore, and SpiderMonkey. The host runtime embeds the engine and controls the environment around it. For example, Node.js and Deno both use V8, Bun uses JavaScriptCore, Firefox uses SpiderMonkey, and Safari uses JavaScriptCore.
What is the event loop?
The event loop is the runtime mechanism that schedules JavaScript to run in response to events. JavaScript code is generally processed one task at a time and runs to completion before the next task begins. Events might include timers expiring, network data arriving, file-system operations completing, or a user clicking a button in a browser.
Are microtasks, nextTicks, and immediates the same thing?
No. They are different kinds of queued work. The microtask queue is defined by the JavaScript language and is used for promises. Node.js also provides a nextTick queue through process.nextTick() and an immediates queue through setImmediate(). These Node.js-specific queues are host-defined, so other runtimes may not support them or may implement them differently.
What is the difference between language-defined, implementation-defined, and host-defined behavior?
Language-defined behavior is specified by ECMAScript and should behave consistently across runtimes, such as promises. Implementation-defined behavior depends on the JavaScript engine, so runtimes using the same engine may behave similarly. Host-defined behavior is controlled by the runtime itself, such as Node.js-specific APIs like process.nextTick(), and may vary significantly between Node.js, Deno, Bun, browsers, and other environments.
JavaScript in Depth ebook for free