Error handling
Error handling
Section titled “Error handling”Every error the server surfaces to the client is a structured object with
four guaranteed fields: a stable code, a retriable boolean, a
human-readable message, and a recovery_hint that tells the agent what
to do next. This page is the map of where those errors come from.
The hierarchy
Section titled “The hierarchy”DiscordMcpError (base — all custom errors extend this)├─ DiscordClientError (caller's fault — 4xx-equivalent)│ ├─ DiscordPermissionError (DISCORD_PERMISSION_DENIED)│ ├─ DiscordRateLimitError (DISCORD_RATE_LIMITED)│ ├─ DiscordNotFoundError (DISCORD_NOT_FOUND)│ ├─ ValidationError (VALIDATION_FAILED)│ ├─ DiscordAuthError (DISCORD_AUTH_INVALID)│ ├─ DiscordCloudflareBlocked (DISCORD_CLOUDFLARE_BLOCKED)│ ├─ ScopeRejectedError (SCOPE_REJECTED)│ ├─ GuildNotAllowedError (GUILD_NOT_ALLOWED)│ ├─ DryRunPreview (DRY_RUN_PREVIEW)│ └─ CancelledError (CANCELLED)└─ DiscordServerError (server's fault — 5xx-equivalent) ├─ CircuitOpenError (CIRCUIT_OPEN) ├─ BulkheadFullError (BULKHEAD_SATURATED) ├─ DiscordTimeoutError (DISCORD_TIMEOUT) └─ DiscordRestUnavailable (DISCORD_REST_UNAVAILABLE)Source:
errors/base.ts,
errors/client.ts,
errors/server.ts.
The split mirrors HTTP semantics: client errors are deterministic (the same input will fail the same way; the agent must change the input or the config); server errors are transient (retry might work).
formatErrorForUser
Section titled “formatErrorForUser”Source: errors/format.ts.
The single conversion point that turns any thrown error into the wire shape returned to the MCP client:
{ code: string, // stable machine-readable identifier retriable: boolean, // whether a naive retry might succeed message: string, // human-readable summary recovery_hint: string, // imperative next step // optional, error-specific: suggested_tool?: string, retry_after_ms?: number, missing_permissions?: string[], validation_issues?: { path: string, message: string, code: string }[],}Any DiscordMcpError formats trivially via this helper. Unknown errors
(e.g. a programmer mistake that throws a TypeError) are wrapped into a
generic INTERNAL_ERROR shape with retriable: false and a hint that
mentions filing an issue — we never leak stack traces to the client.
Recovery hints
Section titled “Recovery hints”The contract: every recovery_hint is imperative + actionable for the
agent.
| Code | Recovery hint shape |
|---|---|
DISCORD_PERMISSION_DENIED | ”Grant <perm> to bot’s role in Server Settings → Roles.” |
DISCORD_RATE_LIMITED | ”Wait <ms>ms then retry” (+ optional batch alternative). |
DISCORD_NOT_FOUND | ”Verify: 1) <resource> exists 2) bot has VIEW permission 3) ID is correct” |
VALIDATION_FAILED | ”Fix <path>: <zod issue>” |
DRY_RUN_PREVIEW | ”Set MCP_DRY_RUN=false AND pass __confirm:true to actually execute” |
CIRCUIT_OPEN | ”Discord upstream failing; circuit auto-recovers in <ms>ms” |
BULKHEAD_SATURATED | ”Too many in-flight calls; retry after current batch completes” |
The hints encode the fix, not the diagnosis. The agent doesn’t need to
know what a bulkhead is to act on BULKHEAD_SATURATED — it just needs to
know “retry later.”
The DRY_RUN_PREVIEW pattern
Section titled “The DRY_RUN_PREVIEW pattern”The DryRunPreview error is the load-bearing piece of the safe-by-default
posture. When a destructive tool is called without confirmation, the
precondition throws DryRunPreview carrying the redacted preview of what
would have run:
{ "code": "DRY_RUN_PREVIEW", "retriable": false, "message": "Dry-run: would call messages_send with the given args", "recovery_hint": "Set MCP_DRY_RUN=false AND pass __confirm:true (or use elicitation flow) to actually execute", "preview": { "channel_id": "111122223333444455", "content": "..." }}The agent sees the preview, optionally surfaces it to the human, then either:
- Re-issues the call with
__confirm:true(and the operator hasMCP_DRY_RUN=falsein env), or - Surfaces the preview as “this is what I would do, are you sure?” via elicitation if the client supports it.
Either way, no Discord call ever fires until both halves of the contract are satisfied. See Confirmation for the contract details.
Cockatiel mappings
Section titled “Cockatiel mappings”The resilience layer uses Cockatiel for retry, circuit breaker, bulkhead, and timeout. Cockatiel throws its own error types; we map them to discord-mcp errors so the client never sees a leaky implementation detail:
| Cockatiel error | discord-mcp error | Code |
|---|---|---|
BrokenCircuitError | CircuitOpenError | CIRCUIT_OPEN |
BulkheadRejectedError | BulkheadFullError | BULKHEAD_SATURATED |
TaskCancelledError (timeout) | DiscordTimeoutError | DISCORD_TIMEOUT |
Mapping happens in rest/errors.ts
inside the resilient REST adapter, so all REST callers see the canonical
codes.
This was added in Plan 8 Phase D — before that, callers occasionally got
BrokenCircuitError thrown straight through, which broke the
formatErrorForUser contract because Cockatiel’s error class isn’t a
DiscordMcpError.
The tool_error envelope
Section titled “The tool_error envelope”Errors don’t always cross the wire as JSON-RPC errors. By design, the tool
result envelope can carry isError: true with structured content,
preserving the request as a successful protocol exchange while marking
the application result as failed:
{ "isError": true, "structuredContent": { "code": "DISCORD_NOT_FOUND", "retriable": false, "recovery_hint": "Verify: 1) ...", "message": "Channel 999... not found" }}Why: an MCP-level JSON-RPC error implies “the protocol exchange itself
broke.” A 404 from Discord is not a protocol break — it’s a perfectly
ordinary outcome the agent needs to handle. Returning it as
isError: true with structured content keeps the trace clean (the call
succeeded; the result said no) and lets the agent branch on the code
field.
Source map
Section titled “Source map”| Concern | File |
|---|---|
| Base classes | errors/base.ts |
| Client errors | errors/client.ts |
| Server errors | errors/server.ts |
| Wire formatter | errors/format.ts |
| Cockatiel mappings | rest/errors.ts |
Related
Section titled “Related”- Architecture → Confirmation — the
DRY_RUN_PREVIEWcontract. - Operations → Resilience — what triggers the Cockatiel error mappings.
- Architecture → Rate limits —
DISCORD_RATE_LIMITEDrecovery semantics.