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):

Gateway (connection lifecycle and protocol):

REST:

Decode:

Internal:

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

Types

pub opaque type BucketId
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

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(value: String) -> BucketId
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
Search Document