The QuickJS Sandbox
Minecraft Bedrock does not run Node.js or a browser engine like V8. Instead, it embeds QuickJS, a lightweight, standards-compliant JavaScript engine built for fast startup and low memory overhead. Your code runs in a strict ES2020 module environment where every file is its own scope and imports must be explicitly declared.
Because this is a closed game sandbox rather than a general-purpose runtime, standard platform globals do not exist. You cannot access the local file system, open raw network sockets, or use the window or process objects. Even familiar timers like setTimeout and setInterval are absent; all time-based scheduling must go through the game loop using the system object provided by the server module.
| Feature / Global | Availability | Bedrock Equivalent |
|---|---|---|
| ES6 Modules (import/export) | Supported | Native support via BP/scripts/ |
| console.log / console.warn | Supported | Outputs to Content Log UI and log file |
| setTimeout / setInterval | Missing | system.runTimeout / system.runInterval |
| fs / File System APIs | Missing | Dynamic Properties for persistent data |
| fetch / XMLHttpRequest | Dedicated Server Only | @minecraft/server-net (Experimental) |
Manifest Configuration
Before the engine initializes your script environment, your behavior pack manifest must declare a script module and its explicit library dependencies. The entry field points to the root JavaScript file inside your pack, which must live inside the scripts folder.
Every native module you import in code, such as @minecraft/server, must have a matching entry in the dependencies array. If you use a stable version string, the script runs in any standard world. If you append the -beta suffix to access experimental APIs, the world must have the Beta APIs toggle enabled or the engine will refuse to construct the environment.
{
"format_version": 2,
"header": {
"name": "Runtime Environment Demo",
"description": "Demonstrates script engine setup",
"uuid": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d",
"version": [1, 0, 0],
"min_engine_version": [1, 20, 80]
},
"modules": [
{
"description": "Script Module",
"type": "script",
"language": "javascript",
"uuid": "f6e5d4c3-b2a1-4f5e-9d8c-7b6a5f4e3d2c",
"version": [1, 0, 0],
"entry": "scripts/main.js"
}
],
"dependencies": [
{
"module_name": "@minecraft/server",
"version": "1.10.0"
}
]
}Tick Lifecycle and Read-Only Execution
The Bedrock engine advances the world at a fixed rate of 20 ticks per second (one tick every 50 milliseconds). Your JavaScript code executes synchronously on the server thread during a specific slice of that tick. It does not run in parallel with entity movement or block updates.
To keep the internal C++ game state safe during event processing, the engine enforces two distinct execution modes: normal mode and read-only mode. When a beforeEvents listener fires—or while the file is initially loading before the world is ready—the engine locks the world into read-only mode. In this state, you can read properties and cancel the incoming event, but any method that mutates the world (like spawning an entity, setting a block, or adding an effect) will immediately throw a privilege error.
To run state-changing code from a read-only context, you must defer execution until later in the tick using system.run(). This queues a callback to run during the normal script phase when world mutation is unlocked.
import { world, system } from "@minecraft/server";
// beforeEvents run in Read-Only mode
world.beforeEvents.itemUse.subscribe((event) => {
const player = event.source;
if (event.itemStack.typeId === "minecraft:stick") {
// Cancel the default item use while still in beforeEvents
event.cancel = true;
// Defer world modification to the normal execution phase
system.run(() => {
player.dimension.spawnEntity("minecraft:lightning_bolt", player.location);
player.sendMessage("Struck lightning safely outside read-only mode.");
});
}
});The Watchdog, Memory Limits, and Jobs
Because scripts share the main server thread, a slow script directly delays the tick for all players. To protect the game from freezing, the engine runs a strict performance monitor called the Watchdog. If your script spends too many milliseconds in a single tick, or allocates memory beyond the pack cap, the Watchdog intervenes and terminates your script module for the rest of the session.
When you need to perform heavy work—like scanning thousands of blocks or generating a procedural structure—you cannot use a standard synchronous loop. Instead, wrap the work in a JavaScript generator function and pass it to system.runJob(). The engine will execute your generator in small increments across multiple ticks, pausing automatically whenever the current tick runs out of spare time.
import { world, system } from "@minecraft/server";
function* clearVolumeGenerator(dimension, startPos, size) {
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
for (let z = 0; z < size; z++) {
const block = dimension.getBlock({
x: startPos.x + x,
y: startPos.y + y,
z: startPos.z + z
});
if (block && !block.isAir) {
block.setType("minecraft:air");
}
// Yield control back to the engine after every block check
yield;
}
}
}
}
export function startSafeClear(player) {
const job = clearVolumeGenerator(player.dimension, player.location, 16);
system.runJob(job);
}State Persistence Across Reloads
Global variables and Maps in your JavaScript files only live in volatile RAM. As soon as the player saves and quits, or a dedicated server restarts, the entire QuickJS runtime is torn down and your variables reset. Furthermore, the `/reload` command completely destroys and recreates the script environment on the fly.
If you need data to survive across sessions, do not rely on runtime memory. Store primitive values (strings, numbers, booleans, or Vector3 objects) on the world, entities, or item stacks using the Dynamic Properties API. Dynamic properties are serialized directly into the world's LevelDB storage alongside native game data.
Which versions this applies to
Applies to Bedrock Edition 1.19.50 and newer. The core runtime, system.run(), and system.runJob() are stable in @minecraft/server 1.4.0+ and require no experimental toggles unless you specify a -beta dependency version in manifest.json.
When it does not work
| What you see | Why | The fix |
|---|---|---|
| ReferenceError: ' spawnEntity ' does not have required privileges. (or 'Cannot modify the world in read-only mode') | You called a method that alters the world state inside a world.beforeEvents callback or at the top level of your script file during initial load. | Wrap the state-changing code inside a system.run(() => { ... }) block, or switch your listener from world.beforeEvents to world.afterEvents if you do not need to cancel the event. |
| [Scripting][error]-Watchdog: [Pack Name] Hang detected / Script execution timed out | A synchronous while or for loop ran for too many iterations in a single tick, exceeding the engine's millisecond spike threshold. | Convert the loop into a generator function (function*) with a yield statement inside the inner loop, and execute it with system.runJob(). |
| Failed to load script module: Unable to resolve module '@minecraft/server' version '...' | Either the version number in manifest.json does not exist in the current game build, or you used a '-beta' version string without enabling the Beta APIs experimental toggle on the world. | Use a known stable version string (like '1.10.0') for standard worlds, or turn on the 'Beta APIs' experiment in the world settings if you need beta features. |
| SyntaxError: cannot use import statement outside a module / Could not find module './utils' | QuickJS requires explicit file extensions in relative import paths when loading secondary script files. | Always include the '.js' extension at the end of relative imports, for example: import { helper } from './utils.js'; |
Reference, straight from the game files
Read out of the game itself, so the ids match the version you are building for.
