tadpole/error
Tadpole’s error type. Variants carry structured context — route, status, retry-after, decode path — so callers match on them instead of parsing strings; error/render.gleam turns them into human-readable messages, with the token redacted everywhere it could surface.
The taxonomy, grouped by subsystem
Config (before anything connects — tadpole.validate,
bot.start/run):
MissingToken,InvalidTokenFormat(got)— empty or malformed token;gotis redacted when rendered.IntentsNotPrivileged(intent)— the one named intent needs a Developer Portal toggle.ShardingNotSupported(got)— this milestone runs one shard.
Gateway (connection lifecycle and protocol):
GatewayConnectFailed(reason, attempt, next_retry_ms)— one connect attempt failed; the shard retries on its own.reasonis aConnectReason(below).GatewayClosedUnexpectedly(close_code, can_resume, session_id, sequence)— a live connection closed; the fields say whether the session can resume.HeartbeatAckMissed/HeartbeatTimeout— the zombie detectors: ACKs missing, round-trip overdue.IdentifyFailed/ResumeFailed— Discord refused the handshake step; the reason is Discord’s.
REST:
RestStatus(route, status, discord_code, body)— any non-2xx. Status 0 is the transport-failure convention: no HTTP response happened (tadpole/rest/execute explains why no separate variant).RateLimited(route, retry_after_ms, is_global, bucket)— 429s outlasted the retries.
Decode:
DecodeFailed(event, path, expected, got)— a payload stopped matching its decoder;pathis the JSON path,gottruncated.UnknownEvent(event_name, opcode)— reserved for unmodeled events; nothing raises it today, the typed event layer decodes them toUnknowndata instead.
Internal:
InternalContractViolation(location, details, cause)— a tadpole bug found at runtime. Not your code’s fault; the renderer asks for a report.
Route and BucketId
Route(method, path) identifies the REST call in errors — printed
as POST /channels/123/messages by route_to_string.
BucketId wraps Discord’s X-RateLimit-Bucket string, the identity
rate-limit state is keyed by once a response names it; both are
opaque so error payloads cannot be silently reshaped.
ConnectReason names why a gateway connect failed — from
InvalidToken to SessionStartLimited to NetworkError — with
Other(String) catching anything new.
Failure modes
Pure data and total functions — this module cannot fail.
redact_token keeps the first 4 and last 4 characters, [redacted]
under 9. What the variants mean is above; the renderer is
tadpole/error/render.
See also
tadpole/error/render— every variant to texttadpole/guide— matchingRateLimitedfor control flow
Types
pub type ConnectReason {
InvalidToken
BadRequest
Unauthorized
Forbidden
NotFound
UnsupportedVersion
SessionStartLimited
NetworkError
TlsError
ServiceUnavailable
Other(String)
}
Constructors
-
InvalidToken -
BadRequest -
Unauthorized -
Forbidden -
NotFound -
UnsupportedVersion -
SessionStartLimited -
NetworkError -
TlsError -
ServiceUnavailable -
Other(String)
pub type HttpMethod {
GET
POST
PUT
DELETE
PATCH
HEAD
OPTIONS
}
Constructors
-
GET -
POST -
PUT -
DELETE -
PATCH -
HEAD -
OPTIONS
pub type IntentName {
GuildMembersIntent
GuildPresencesIntent
MessageContentIntent
}
Constructors
-
GuildMembersIntent -
GuildPresencesIntent -
MessageContentIntent
pub type Route {
Route(method: HttpMethod, path: String)
}
Constructors
-
Route(method: HttpMethod, path: String)
pub type TadpoleError {
MissingToken
InvalidTokenFormat(got: String)
IntentsNotPrivileged(intent: IntentName)
ShardingNotSupported(got: Int)
GatewayConnectFailed(
reason: ConnectReason,
attempt: Int,
next_retry_ms: Int,
)
GatewayClosedUnexpectedly(
close_code: Int,
can_resume: Bool,
session_id: option.Option(String),
sequence: option.Option(Int),
)
HeartbeatAckMissed(expected_acks: Int, received_acks: Int)
HeartbeatTimeout(interval_ms: Int)
IdentifyFailed(reason: String)
ResumeFailed(reason: String)
RestStatus(
route: Route,
status: Int,
discord_code: option.Option(Int),
body: option.Option(String),
)
RateLimited(
route: Route,
retry_after_ms: Int,
is_global: Bool,
bucket: option.Option(BucketId),
)
DecodeFailed(
event: option.Option(String),
path: String,
expected: String,
got: String,
)
UnknownEvent(event_name: String, opcode: Int)
InternalContractViolation(
location: String,
details: String,
cause: option.Option(String),
)
}
Constructors
-
MissingToken -
InvalidTokenFormat(got: String) -
IntentsNotPrivileged(intent: IntentName) -
ShardingNotSupported(got: Int)bot.start runs exactly one shard this milestone; a config asking for more is refused before any connection is attempted.
-
GatewayConnectFailed( reason: ConnectReason, attempt: Int, next_retry_ms: Int, ) -
GatewayClosedUnexpectedly( close_code: Int, can_resume: Bool, session_id: option.Option(String), sequence: option.Option(Int), ) -
HeartbeatAckMissed(expected_acks: Int, received_acks: Int) -
HeartbeatTimeout(interval_ms: Int) -
IdentifyFailed(reason: String) -
ResumeFailed(reason: String) -
RestStatus( route: Route, status: Int, discord_code: option.Option(Int), body: option.Option(String), ) -
RateLimited( route: Route, retry_after_ms: Int, is_global: Bool, bucket: option.Option(BucketId), ) -
DecodeFailed( event: option.Option(String), path: String, expected: String, got: String, ) -
UnknownEvent(event_name: String, opcode: Int) -
InternalContractViolation( location: String, details: String, cause: option.Option(String), )
Values
pub fn bucket_id_to_string(bucket: BucketId) -> String
pub fn connect_reason_to_string(reason: ConnectReason) -> String
pub fn method_to_string(method: HttpMethod) -> String
pub fn redact_token(token: String) -> String
First 4 and last 4 chars kept, everything between hidden. Short values become [redacted]. Tests assert the full token never renders.
pub fn route_to_string(route: Route) -> String