Debugging Scripts
Scripts run in isolated Web Workers. This means you can’t use the browser
console, Deno’s --inspect flag, or filesystem access. Here’s how to diagnose
and fix issues.
Overview
When a script fails, the error is:
- Caught by the sandbox
- Sent back as an
errormessage from the worker - Logged to the server console with
[Sandbox] error: - Not shown to the player (the script simply produces no output)
So a script that silently does nothing is often one that threw an exception.
Common Mistakes
1. Missing await on async methods
Wrong:
const gold = u.me.state.gold as number || 0;
u.db.modify(u.me.id, "$set", { "data.gold": gold + 10 }); // ← missing await!
u.send(`Gold updated.`);
Correct:
const gold = u.me.state.gold as number || 0;
await u.db.modify(u.me.id, "$set", { "data.gold": gold + 10 });
u.send(`Gold updated.`);
Missing await means the DB write starts but the script may exit before it
completes. In practice you’ll often see partial results or no effect.
2. Wrong u.db.modify() operator
Wrong:
await u.db.modify(u.me.id, "state", { gold: 100 }); // "state" is not a valid op
await u.db.modify(u.me.id, "name", "Alice"); // "name" is not a valid op
Correct:
await u.db.modify(u.me.id, "$set", { "data.gold": 100 });
await u.db.modify(u.me.id, "$set", { name: "Alice" });
Valid ops: "$set", "$unset", "$inc". Any other string is silently rejected.
3. Missing await on u.canEdit()
Wrong:
if (!u.canEdit(u.me, target)) { // ← returns Promise, not boolean!
u.send("Permission denied.");
return;
}
Correct:
if (!(await u.canEdit(u.me, target))) {
u.send("Permission denied.");
return;
}
4. Clobbering state on modify
Wrong:
// This wipes all other fields in data!
await u.db.modify(u.me.id, "$set", { data: { gold: 100 } });
Correct:
// Spread to preserve existing fields
await u.db.modify(u.me.id, "$set", { data: { ...u.me.state, gold: 100 } });
// Or use dot-notation to target one field:
await u.db.modify(u.me.id, "$set", { "data.gold": 100 });
5. ESM import that doesn’t work in workers
Wrong:
import { sprintf } from "jsr:@std/fmt/printf"; // ← JSR sub-path, won't resolve
Correct:
// Use u.util.sprintf instead — it's already in the sandbox
u.util.sprintf("%-10s %5d", "Score", 100);
Standard ESM imports (import X from "https://...") work, but jsr: package
sub-paths do not resolve in Web Worker context. Use u.util helpers instead.
6. Assuming u.db.search() returns one item
const player = await u.db.search({ "data.name": /alice/i });
// player is IDBObj[], not IDBObj!
if (!player) { ... } // wrong — an empty array is truthy
// Correct:
const results = await u.db.search({ "data.name": /alice/i });
if (!results.length) { u.send("Not found."); return; }
const player = results[0];
7. Not checking for null from u.util.target()
const target = await u.util.target(u.me, u.cmd.args[0]);
u.send(`Target: ${target.name}`); // throws if target is undefined!
// Correct:
const target = await u.util.target(u.me, u.cmd.args[0]);
if (!target) { u.send("I don't see that."); return; }
u.send(`Target: ${target.name}`);
Script Errors
Script produces no output
Causes:
- Unhandled exception (check server console for
[Sandbox] error:) - Script returned early due to a condition check
u.send()called with undefined/null message
Diagnose:
// Add a breadcrumb at the start to verify the script is running
u.send("DEBUG: script started");
// Check every early return
const target = await u.util.target(u.me, u.cmd.args[0]);
u.send(`DEBUG: target = ${target?.id ?? "null"}`);
if (!target) { u.send("Not found."); return; }
Script times out
Scripts have a 5-second execution timeout. If your script makes many DB
calls in a loop, it may time out.
Signs: Server logs Script execution timed out
Fix: Batch queries instead of looping:
// Bad: N sequential queries
for (const id of playerIds) {
const p = await u.db.search({ id }); // one query per player
}
// Better: one query
const players = await u.db.search({ location: u.here.id, flags: /player/i });
Worker crashes silently
If the worker throws before any u.send(), the player sees nothing. The server
console will show the error.
Watch for:
ReferenceError: X is not defined— JSR import that failed to resolveTypeError: Cannot read properties of undefined— null/undefined not checkedSyntaxError— TypeScript that failed to transpile
Debugging Techniques
Use think to echo values
The think command (system/scripts/think.ts) sends output to you without
broadcasting. Use it as a debug probe:
think [output of a long expression]
Add temporary u.send() breadcrumbs
export default async (u: IUrsamuSDK) => {
u.send("[DEBUG 1] entering script");
const target = await u.util.target(u.me, u.cmd.args[0]);
u.send(`[DEBUG 2] target = ${target?.id ?? "null"}`);
if (!target) return;
const gold = (target.state.gold as number) || 0;
u.send(`[DEBUG 3] gold = ${gold}`);
await u.db.modify(target.id, "$set", { "data.gold": gold + 10 });
u.send("[DEBUG 4] db.modify done");
};
Remove the debug lines before committing.
Check the server console
The server logs all sandbox errors. Look for lines like:
[Sandbox] error: TypeError: Cannot read properties of undefined (reading 'id')
Use @js for quick experiments (wizard/admin only)
The @js command runs a JavaScript expression directly (no sandbox — full
server access). Useful for testing DB queries:
@js await dbojs.queryOne({ id: "1" })
Testing Scripts Manually
Run the test suite
deno task test
This runs tests in tests/, src/services/Intents/, src/services/Sandbox/,
and src/plugins/events/.
Write a script test
Tests for scripts follow this pattern — read the file, strip the export,
inject a stub u object, and run the code:
import { assertEquals } from "jsr:@std/assert";
const OPTS = { sanitizeResources: false, sanitizeOps: false };
Deno.test("greet script sends a message", OPTS, async () => {
const sent: string[] = [];
const u = {
me: { id: "1", name: "Alice", flags: new Set(["player"]), state: {}, contents: [] },
here: { id: "2", name: "Hall", state: {}, contents: [], flags: new Set(["room"]),
broadcast: () => {} },
cmd: { name: "greet", args: ["Bob"], switches: [] },
state: {},
send: (msg: string) => { sent.push(msg); },
util: {
target: async () => ({ id: "3", name: "Bob", flags: new Set(["player"]),
state: {}, contents: [] }),
displayName: (obj: { name?: string }) => obj.name ?? "Unknown",
},
db: {
search: async () => [],
modify: async () => {},
},
} as unknown;
// Load and run the script
const src = await Deno.readTextFile("system/scripts/greet.ts");
const fn = new Function("u", src.replace(/^export default/, "return"));
await fn(u)(u);
assertEquals(sent[0], "You wave to Bob.");
});
Plugin Debugging
Check if your plugin loaded
Look for your plugin’s init log in the server console:
[myplugin] Plugin initialized — +myplugin commands active
If missing, the plugin file wasn’t found or threw during import.
Verify addCmd was called
Add a console.log to your commands.ts:
console.log("[myplugin] registering commands");
addCmd({ ... });
This should appear at startup before Plugin initialized.
Route not responding
If your registerPluginRoute handler isn’t called:
- Verify the path starts with
/api/v1/ - Check that
registerPluginRouteis called insideinit(), not at module level - Confirm the auth header is present (
Authorization: Bearer <jwt>)
GameHooks not firing
If gameHooks.on() isn’t triggering:
- Verify you’re calling
on()ininit()and storing the handler reference - Check that
off()isn’t being called before the event fires - Confirm the event name matches exactly (case-sensitive)