Error catalogue — Meta codes + our typed errors
src/utils/meta-errors.ts maps Meta Cloud API integer error codes to typed JS error classes. Tool handlers pattern-match on the typed class; audit logging (Phase 4+) groups failures by class for alerting.
Mapped Meta codes
| Meta code | Typed class | When | Recovery |
|---|---|---|---|
131047 | OutOfSessionWindowError | Sending a plain text message outside the 24h customer-service window | Use a pre-approved template (send_template, deferred to Phase 8) |
131026 | InvalidRecipientError | Recipient phone not on WhatsApp / malformed | Verify to was normalised through toMetaFormat; if so, the recipient is genuinely unreachable |
132xxx | TemplateError | Template render / language / status problems | Re-check template body, parameter list, and language code; see incident runbook for approval recovery |
190 | InvalidAccessTokenError | Meta access token invalid or expired | Rotate via the incident runbook (access_token_secret_ref resolves through the secrets layer, so rotation = swap one file + container restart) |
| any | MetaError (base) | Catch-all | Inspect .code, .subcode, .fbtraceId and consult Meta’s error code reference |
Our internal errors
PhoneNormalisationError
Thrown by src/utils/phone.ts when input is empty or yields fewer than 8 digits after stripping non-digit characters. Always surfaces at the tool boundary — the LLM sees a clear “phone must contain at least 8 digits” message and retries with the corrected input.
How tools surface errors
- Validation error (zod) — MCP protocol error code
-32602(InvalidParams). The dispatcher in src/server/mcp.ts handles it. - Typed Meta error — thrown from the handler; the dispatcher wraps it as
{ isError: true, content: [...] }so the LLM gets a tool-level error result, not a protocol failure. This lets the model decide to retry with different args, surface a clean explanation to the user, or escalate. - Out-of-window (131047) — handler throws
Error('Cannot send a plain text message — the 24h customer-service window has elapsed. ...'). Same wrapping; the message is the explanation. - Internal invariant failures — thrown as plain
Error. Logged atwarn/error; the model getsisError: truewith the message.
HTTP-level errors (Phase 5+ remote MCP)
Reserved here for future content. Phase 5 lands the Streamable HTTP transport.
Rate-limit errors (Phase 4+)
| Layer | Shape |
|---|---|
| HTTP | 429 Too Many Requests + Retry-After, X-RateLimit-* headers |
| MCP protocol | JSON-RPC error -32004 with data: { retryAfterSeconds, scope } |
Surfacing rate-limit failures as a protocol error (not a tool error) signals to the model that it should back off, rather than retry the tool with different args.