Wiki 9Minecraft Wiki 9Minecraft

Script Privileges and Read-Only Execution Mode

Understand why Bedrock scripts throw read-only errors and how to defer state mutations with system.run.

5Sections
1Code examples
3Known errors

Script privileges govern when and where your JavaScript code is allowed to modify the Minecraft world state. In the `@minecraft/server` API, certain contexts—specifically `beforeEvents` callbacks—run in a restricted read-only mode to prevent cascading state corruption while the engine is still processing an action. Knowing how to navigate these privilege boundaries and defer world mutations to a privileged tick context is fundamental to writing crash-free Bedrock scripts.

What Are Script Privileges?

When the Bedrock scripting engine executes your code, it assigns an execution privilege level to the current call stack. In standard contexts—such as inside `system.runInterval`, `afterEvents` callbacks, or custom command handlers—your script has full read-write privileges. It can freely spawn entities, change block permutations, modify dynamic properties, or apply knockback.

However, when the engine is in the middle of resolving an internal game mechanic, it locks the world state before invoking your script. During this window, your code can read properties (like checking an entity's health, reading a block's type, or inspecting an item stack), and it can modify the specific event payload passed into the callback (such as setting `event.cancel = true`). Any attempt to call a method or set a property that mutates the broader world will immediately throw a runtime exception and halt the execution of that callback.

Execution Context Comparison

Different entry points into your script carry different privilege states. The table below outlines the primary execution contexts in `@minecraft/server` and what you are permitted to do inside them.

Execution ContextPrivilege StateCan Cancel Event?Can Mutate World?
world.beforeEvents.*Read-Only (Restricted)Yes (via event.cancel = true)No — throws Native Function error
world.afterEvents.*Read-Write (Full)No (action already occurred)Yes
system.beforeEvents.watchdogTerminateRead-Only (Restricted)Yes (via event.cancel = true)No
system.run / system.runTimeout / system.runIntervalRead-Write (Full)N/AYes
Top-level script initializationLimited (World not loaded)N/ANo — wait for worldInitialize or first tick

The beforeEvents Read-Only Restriction

The most common place creators encounter privilege restrictions is inside `world.beforeEvents`, such as `playerBreakBlock`, `itemUse`, or `chatSend`. These events fire after a player or entity initiates an action, but before the game engine commits the result to the world.

Because the engine's internal C++ state is mid-transition during a `beforeEvent`, allowing scripts to delete the acting entity, overwrite the block being broken, or teleport the player would invalidate memory pointers the engine is actively holding. To prevent hard crashes, Mojang marks all world-mutating APIs as requiring full privileges. If you call `player.teleport()`, `dimension.spawnEntity()`, or `entity.addEffect()` directly inside a `beforeEvents` listener, the call is blocked.

Escaping Read-Only Mode with system.run

When you need to both cancel an action (which requires a `beforeEvent`) and mutate the world in response (which requires read-write privileges), you bridge the gap using `system.run()`. Calling `system.run(callback)` is safe inside read-only mode because it does not mutate the world; it merely queues a JavaScript function to run during the scripting phase of the current or next game tick, once full privileges are restored.

One critical detail when deferring execution out of a `beforeEvent` is that the event object's properties may be invalidated or recycled once the event finishes firing. Furthermore, object references like `event.block` continue to point to a specific coordinate in the dimension, meaning if the block changes before `system.run` executes, reading it later yields the new state. Always capture primitive values (strings, numbers, vector coordinates) or persistent references (like the `Player` object) into local variables before entering the `system.run` closure.

BP/scripts/main.js
import { world, system } from "@minecraft/server";

world.beforeEvents.playerBreakBlock.subscribe((event) => {
    const { player, block, itemStack } = event;

    // Reading properties is allowed in read-only mode
    if (block.typeId === "minecraft:diamond_ore" && itemStack?.typeId === "minecraft:wooden_pickaxe") {
        // Modifying the event payload is allowed in read-only mode
        event.cancel = true;

        // Capture coordinates before the event context closes
        const blockLoc = { x: block.location.x, y: block.location.y, z: block.location.z };
        const dim = block.dimension;

        // Defer world mutations to a privileged tick context
        system.run(() => {
            // Now in Read-Write mode: mutating the world and player is safe
            dim.setBlockType(blockLoc, "minecraft:cobblestone");
            player.sendMessage("§cYour wooden pickaxe shattered the diamond into cobblestone!");
            player.playSound("random.break");
        });
    }
});

Top-Level Script Initialization Limits

Another subtle privilege boundary occurs when your script file is first imported and evaluated at the very start of world load. During this top-level execution phase, you can subscribe to `beforeEvents` and `afterEvents`, and you can schedule `system.runInterval` timers. However, the world dimensions and entities have not finished loading.

Attempting to query `world.getDimension("overworld").getEntities()` or set world dynamic properties directly at the top level of `main.js` will either fail or return empty results. If you need to run one-time setup logic that mutates or queries the world upon load, wrap it in `world.afterEvents.worldInitialize.subscribe()` or defer it with `system.run()`.

Which versions this applies to

Applies to Bedrock Edition 1.20.0 and newer using stable `@minecraft/server` versions (1.4.0+). In older 1.19.x experimental versions, `beforeEvents` were located under `world.events.before*`, though the read-only restriction and `system.run` workaround functioned identically. No experimental toggles are required for standard event privilege handling in current versions.

When it does not work

What you seeWhyThe fix
ReferenceError: Native function [Entity::addEffect] does not have required privileges.You called a method that modifies an entity, block, or dimension directly inside a `world.beforeEvents.*` callback, while the scripting engine is locked in read-only mode.Wrap the mutating method calls inside `system.run(() => { ... })` so they execute after the `beforeEvent` finishes, or switch your listener to the corresponding `world.afterEvents.*` event if you do not need to set `event.cancel = true`.
Setting `event.cancel = true` inside `system.run()` does not stop the block from breaking or the chat message from sending.`system.run()` delays execution until after the `beforeEvent` has already completed and the engine has committed the action to the world. By the time the callback inside `system.run()` fires, it is too late to cancel the event.Keep `event.cancel = true` synchronous at the top level of the `beforeEvents` listener, and only put the world-mutating side effects (like spawning entities or setting blocks) inside `system.run()`.
Properties read from `event.itemStack` or `event.block` inside `system.run()` are undefined or reflect the wrong block.Event data objects are only guaranteed to be valid for the duration of the synchronous event callback. By the time `system.run()` executes on the tick, the block at that location may have already been broken or replaced.Read and store primitive values (like `const typeId = event.block.typeId;` and `const loc = event.block.location;`) in constants before calling `system.run()`, then reference those constants inside your deferred callback.

Reference, straight from the game files

Read out of the game itself, so the ids match the version you are building for.