Scripting Guide
Lesson 12: Error Handling, Limits, and Performance
How failures surface in GuildScript, the limits every run lives under, and how to write agents that behave well at the edges.
The two kinds of failure
GuildScript separates failures into two channels, and knowing which is which saves hours of debugging:
- Action failures do not throw. Every Discord action (
reply,roles.add,channels.fetch, ...), everydbcall, and everyllmcall catches its own errors and returns a value with anerrorproperty (or a fallback likenullor[]). Your code keeps running. - Script crashes throw. A bug in your own code (calling a method on
undefined, a thrown exception, a missing modal field) aborts the run and is reported to the agent's error channel.
Checking action results
export async function onMessage(message) {
if (message.author.bot) return;
if (message.content !== "!cleanup") return;
const deleted = await message.channel.bulkDelete(10);
// On success bulkDelete returns a number. On failure (e.g. missing
// Manage Messages) it returns { error, code } instead.
if (typeof deleted !== "number") {
await message.reply("Could not clean up: " + deleted.error);
return;
}
await message.channel.send("Removed " + deleted + " messages.");
}The rule of thumb: anything you depend on next, check. Sends you do not depend on can be fire-and-forget (still awaited), since a failed send simply returns an error object.
The error channel
When creating or updating an agent in /agents, set the optional Error Channel. Uncaught exceptions in that agent arrive there as an embed with the agent's name, its event, and the error message. Reports are throttled to one per agent per two minutes, so a crash loop does not flood the channel; fix the first report and trigger the event again.
| Error name you may see | Meaning |
|---|---|
RateLimitError with QUOTA LIMIT EXCEEDED | The server hit one of its run quotas (see below). Agents resume when the window refills. |
ExecutionTimeout | The run exceeded the time limit and was terminated. |
SandboxCrash | The script crashed the runtime, usually circular or extremely nested data passed to db/llm/Discord calls. |
Overloaded | The platform's run queue was full; the run was shed. Try again shortly. |
Anything else (TypeError, ...) | A plain bug in the agent's code, with the message attached. |
The limits every run lives under
| Limit | Free | Premium |
|---|---|---|
| Execution time per run | 3.5 s | 10 s |
| Execution time when an LLM provider is connected | 8 s (5 s + 3 s) | 15 s (10 s + 5 s) |
| Agents running in parallel (per server) | 2 | 8 |
| Memory per run | 20 MiB | 96 MiB |
| Runs per second / minute (per server) | 3 / 400 | 100 / 1,000 |
| Runs per hour / day (per server) | 650 / 800 | 5,000 / 10,000 |
| Script file size | 24 KiB | 128 KiB |
Every agent execution counts as one run, whether it does anything or not. An agent that returns on the first line still consumed a run, which is why event choice matters: ten agents on messageCreate burn ten runs per message.
/profile shows usage bars for the minute, hour, and day windows of the current server, so you can see how close your agents run to the edge before users notice anything.
Writing well-behaved agents
- Return early, and first. Put the cheapest checks (author.bot, prefix, channel) at the top; most runs should end within a few lines.
- Prefer payload data over fetches.
message.member.roleIdsis already in the payload;guild.members.fetch()is a network round trip. - Batch reads. One
db.findbeats tendb.findOnecalls in a loop. - Keep loops bounded. You cannot sleep or wait; long loops just eat your 3.5 seconds.
- Do not pass payload objects into db/llm. Extract the plain fields you need; deep or circular structures can crash the sandbox bridge.
- Consolidate agents. One
messageCreateagent with several commands inside spends one run per message; five separate agents spend five.
Debugging workflow
- Reproduce with the error channel set; read the exact exception.
- If nothing arrives, the agent probably returned early or the quota is exhausted; check
/profileand your filter conditions. - Use temporary debug replies (
await message.channel.send(JSON.stringify(value).slice(0, 1900))) to inspect data shapes; remove them after. - Use Download Agent in
/agentsto be certain which version is live, edit, then Update.
Exercise
Take your Lesson 10 economy agent and harden it: check every db write for .error and reply with a friendly failure message; add a guard that replies try again in a minute when a write fails; and move your cheapest condition to the very first line. Then deliberately break it (call message.member.bogus()) and watch the report arrive in your error channel.