Errors and Troubleshooting¶
All wrapper exceptions inherit from ERLCError, so you can always catch them
with one clause. The transport maps PRC HTTP status codes and PRC error codes to
typed Python exceptions before your code sees them. Catch the most specific
exception first, then fall back to ERLCError for anything unexpected. See the
exception list and diagnostics helpers below for structured error handling.
Exception List¶
| Exception | Parent | Raised when |
|---|---|---|
ERLCError |
Exception |
Base class for wrapper failures. |
APIError |
ERLCError |
Generic non-success PRC response. |
BadRequestError |
APIError |
Status 400 or invalid request details. |
AuthError |
APIError |
Status 403 or auth-related PRC codes. |
PermissionDeniedError |
AuthError |
PRC says key lacks permission. |
NotFoundError |
APIError |
Status 404. |
NetworkError |
ERLCError |
Timeout, DNS, connection, or request transport error. |
RateLimitError |
APIError |
Status 429 or PRC rate-limit error code. |
InvalidCommandError |
BadRequestError |
PRC command error code for invalid command. |
RestrictedCommandError |
PermissionDeniedError |
PRC command is restricted from API execution. |
ProhibitedMessageError |
BadRequestError |
PRC rejected message content. |
ServerOfflineError |
APIError |
PRC reports server offline/unavailable. |
RobloxCommunicationError |
APIError |
PRC/Roblox/module communication failure or server-side issue. |
ModuleOutdatedError |
APIError |
In-game module is out of date. |
ModelDecodeError |
ERLCError |
Typed decoding received an unexpected payload shape. |
Error-code Mapping¶
The transport maps PRC error codes when present:
| PRC signal | Wrapper exception |
|---|---|
Status 404 |
NotFoundError |
Status 429 or code 4001 |
RateLimitError |
Status 403 or codes 2000, 2001, 2002, 2003, 2004 |
AuthError |
Code 3001 |
InvalidCommandError |
Code 3002 or status 422 |
ServerOfflineError |
Code 4002 |
RestrictedCommandError |
Code 4003 |
ProhibitedMessageError |
Code 9998 |
PermissionDeniedError |
Code 9999 |
ModuleOutdatedError |
Codes 1001, 1002, or status >=500 |
RobloxCommunicationError |
Status 400 |
BadRequestError |
| Anything else non-success | APIError |
The same table is public:
from erlc_api.error_codes import explain_error_code
info = explain_error_code(4001)
print(info.name, info.exception.__name__, info.advice)
Common mistake: treating PRC error codes as stable business logic in your app. Use wrapper exception classes where possible and log raw codes for diagnostics.
Error-code Utility¶
Import:
Public helpers:
| Helper | Return type | Purpose |
|---|---|---|
explain_error_code(code_or_error) |
ErrorCodeInfo | None |
Explain a code, mapping, exception, retry flag, and advice. |
list_error_codes(category=None) |
list[ErrorCodeInfo] |
List known codes, optionally by category. |
exception_for_error_code(code, status=None) |
type[APIError] |
Return the wrapper exception class for a code/status. |
ErrorCodeInfo.to_dict() returns JSON-safe data for dashboards or docs.
Base Error Shape¶
ERLCError public members:
| Member | Type | Meaning |
|---|---|---|
message |
str |
Human-readable error message. |
method |
str | None |
HTTP method or DECODE. |
path |
str | None |
API path or decode endpoint label. |
status |
int | None |
HTTP status code. |
status_code |
int | None |
Alias for status. |
error_code |
int | None |
PRC error code when present. |
body_excerpt |
str | None |
Short response-body excerpt. |
Common mistake: logging only str(exc) and discarding structured fields. Store
the attributes too when you build dashboards or alerts.
RateLimitError¶
Extra members:
| Member | Type | Meaning |
|---|---|---|
bucket |
str | None |
Parsed from X-RateLimit-Bucket. |
retry_after |
float | None |
Seconds from Retry-After or response body. |
retry_after_s |
float | None |
Alias for retry_after. |
reset_epoch_s |
float | None |
Parsed from X-RateLimit-Reset. |
from erlc_api import RateLimitError
try:
await client.players()
except RateLimitError as exc:
print("bucket", exc.bucket)
print("retry after", exc.retry_after_s)
Important behavior:
retry_429=Trueby default.- The client retries at most once.
- It sleeps only when
Retry-After, body retry data, or reset time provide timing. - If the second attempt is also rate-limited,
RateLimitErroris raised. rate_limited=Trueis enabled by default and waits before requests using observed headers.
Disable automatic retry:
from erlc_api import AsyncERLC, ERLC
api = AsyncERLC("server-key", retry_429=False) # async
api = ERLC("server-key", retry_429=False) # sync
Common mistakes:
- Assuming automatic retries make polling loops safe. Polling utilities still make API calls and can still be rate-limited.
- Sleeping forever. The wrapper sleeps only once.
ModelDecodeError¶
Signature:
Purpose: report unexpected payload shape while decoding typed models.
Extra members:
| Member | Type | Meaning |
|---|---|---|
endpoint |
str |
Endpoint label used by the decoder. |
expected |
str |
Expected payload shape. |
from erlc_api import ModelDecodeError
try:
players = await client.players()
except ModelDecodeError as exc:
print(exc.endpoint, exc.expected, exc.body_excerpt)
Common mistake: retrying decode errors as if they were network failures. Inspect
the raw payload or use raw=True to see what PRC returned.
Validation Helpers¶
validate_key() and health_check() convert common failures into
ValidationResult:
Statuses:
| Status | Meaning |
|---|---|
ValidationStatus.OK |
Key worked. |
ValidationStatus.AUTH_ERROR |
Auth failed. |
ValidationStatus.RATE_LIMITED |
Rate-limited. |
ValidationStatus.NETWORK_ERROR |
Transport failure. |
ValidationStatus.API_ERROR |
Other API error. |
Common mistake: using validation helpers for every request. They are for health checks and setup flows; normal endpoint calls should use exceptions.
Basic Handling Pattern¶
from erlc_api import ERLCError, RateLimitError
try:
players = await client.players()
except RateLimitError as exc:
print("retry later", exc.retry_after_s, exc.bucket)
except ERLCError as exc:
print("request failed", exc.status_code, exc.error_code, exc.body_excerpt)
else:
print("players", len(players))
Catch specific exceptions first, then ERLCError as the shared base.
Common Exceptions and First Steps¶
| Exception | Typical cause | First check |
|---|---|---|
AuthError |
Missing, invalid, banned, or unauthorized key | Confirm server_key and optional global_key. |
PermissionDeniedError |
Key cannot access the resource | Confirm PRC permissions for that key. |
RateLimitError |
PRC returned 429 or rate-limit code |
Use retry metadata and slow polling. |
InvalidCommandError |
PRC rejected command syntax or payload | Print the normalized command in dry-run first. |
RestrictedCommandError |
Command is not allowed through API | Use a different moderation flow. |
ProhibitedMessageError |
PRC rejected command text | Inspect content rules and message text. |
ServerOfflineError |
Server is offline or unreachable | Retry later or show offline status. |
RobloxCommunicationError |
PRC cannot communicate with Roblox/module | Treat as temporary unless repeated. |
ModuleOutdatedError |
In-game module needs update | Update the ER:LC module. |
ModelDecodeError |
Payload shape did not match models | Retry with raw=True and inspect .body_excerpt. |
User-facing Diagnostics¶
Use erlc_api.diagnostics when errors need to become bot replies, dashboard
messages, or structured API responses:
from erlc_api.diagnostics import diagnose_error
try:
players = await client.players()
except Exception as exc:
diagnostics = diagnose_error(exc)
print(diagnostics.to_dict())
For Discord bots, pair diagnostics with dependency-free Discord payload helpers:
from erlc_api.discord_tools import DiscordFormatter
try:
players = await client.players()
except ERLCError as exc:
diagnostics = diagnose_error(exc)
await ctx.send(**DiscordFormatter().diagnostics(diagnostics).to_dict())
Diagnostics are for presentation. Keep typed exception handling for control flow.
Troubleshooting Auth¶
Use validate_key() for setup screens and diagnostics. It returns a
ValidationResult instead of raising common API errors.
Troubleshooting Commands¶
from erlc_api import cmd
preview = await client.command(cmd.pm("Player", "hello"), dry_run=True)
print(preview.raw["command"])
If dry-run looks correct but PRC rejects the command, handle the specific command exception and show a user-facing message.
Use command flows when a moderation tool needs to preview multiple commands before a human confirms them:
from erlc_api.command_flows import CommandFlowBuilder
flow = (
CommandFlowBuilder("warn-and-pm")
.step("warn Avi RDM")
.step("pm Avi Please read the rules")
.build()
)
print(flow.preview())
Flows validate and preview only. They never send commands.
Troubleshooting Models¶
If typed decoding fails:
Report unexpected payloads with the endpoint, wrapper version, and a redacted sample. Do not log server keys or authorization headers.
Removed Ops Stack¶
v2 intentionally removed public cache, Redis, metrics, request replay, tracing, circuit breaker, request coalescing, and retry-policy machinery. Keep those in your application layer if you need them.
Common Mistakes¶
- Catching only
Exceptionand losing useful retry/auth context. - Retrying every error as if it were a network failure.
- Logging complete headers or request objects with secrets.
- Assuming all non-200 responses are rate limits.
- Treating diagnostics as a retry policy. They explain problems; they do not retry requests.
- Expecting command flows to execute commands automatically.
- Logging only
str(exc)and discarding structured fields likestatus_codeanderror_code. - Retrying decode errors as if they were network failures.
- Using validation helpers for every request instead of only setup/health checks.
Related Pages¶
- Rate Limits, Retries, and Reliability
- Security and Secrets
- Testing and Mocking
- Workflow Utilities Reference
Previous Page: Scaling Your App | Next Page: Testing and Mocking