Skip to main content
The SDK has one error class per failure domain, so instanceof tells you what went wrong without parsing a message. For the practical pattern, see Handling errors.
A denied send() is none of these. It arrives on the sendRejected event, because send() is fire-and-forget.

CloakActionError

The typed rejection for an awaited action the server refused.
string
A human-readable explanation, for example missing the kick-members permission or ban reason too long (max 256 chars). This is the stable field to show or log.
PermissionName | undefined
The named permission the bot was denied on, when the failure maps to one. Deleting another member’s message without message_manage sets this to 'message_manage'. It is undefined when the failure does not map to a named permission, so always fall back to message.
number
A raw numeric diagnostic identifying the action. For logging only.
number
A raw numeric diagnostic for the specific failure. For logging only.
Treat opcode and code as opaque. Branch on permission, show message, and never hardcode a numeric code.Three success and error conventions coexist on the Cloak wire, so a raw code you read out of a log means different things depending on the action. Messaging actions deny with negative codes. Moderation actions deny with positive codes 2 and up. The webhook, DM, and REST-credential actions use 0 as success with negative errors. A 0 is not universally a failure and a 2 is not universally a success.

Which calls reject with it

Every awaited action the server can refuse:
  • Message actions: react, unreact, edit, delete, pin, unpin.
  • Moderation: kick, ban, unban, timeout, removeTimeout, fetchBans.
  • Channels and groups: createChannel, editChannel, deleteChannel, moveChannel, createGroup, editGroup, deleteGroup.
  • Reads: fetchRoles, fetchEmojis, and fetchMessages when read history is denied.
  • Direct messages: sendDM, on the create and key-exchange legs. An ineligible target is refused here.
  • Presence: setStatus.
  • Voice: joinVoice and leaveVoice.
  • Commands: sendCommand.
  • Webhook CRUD: webhooks.list, .create, .edit, .regenerate, .delete.
  • REST: client.rest.*, when the credential mint is refused.
fetchMembers is the exception that catches people out. It does not reject at all against any current backend: the promise never settles, so try/catch cannot help you and await hangs. Do not call it.
send(), sendDM() after the DM exists, msg.reply(), msg.channel.send(), sendTyping(), and setAvatar() are fire-and-forget for delivery. Their promises resolve when the frame goes out. See Denied sends.

When there is no named permission

Some denials are permission-shaped but do not map to one of the 31 named permissions. Pinning is the clearest example: it is gated by a channel-level pin permission that has no name in the vocabulary. permission is undefined there, and message still explains what happened.

Denied sends

send() has no success acknowledgement on the wire, so its promise resolves once the frame is out. A server-side denial arrives afterwards, on the sendRejected event.
The payload is { code, message, permission?, serverId, channelId, frame }, with the same message and permission semantics as CloakActionError.
The deny frame carries no routing, so serverId and channelId are attributed best-effort from the most recent send inside a 30 second window and are null otherwise.sendRejected is not an acknowledgement channel. Silence does not mean delivered.
sendRejected does not also fire error. A backend denial is an expected outcome, not a plumbing failure.

CloakClientError

Something failed inside the SDK’s own plumbing: one of your handlers threw, a key pull died, or an inbound frame could not be dispatched. It surfaces on the error event rather than as a rejection, because there is no call for it to reject.
CloakErrorSource
Where it came from: 'listener', 'frame', 'keys', 'transport', or 'reconnect'.
string | undefined
The event name whose listener threw when source is 'listener'. Otherwise a label for the failing call site.
unknown
The original failure, never swallowed and never wrapped twice.
Registering the listener changes no control flow. It only makes failures visible. With no error listener, the behavior is what it was before the event existed: silent, plus a warning under debug.

CloakEnvironmentError

The host is wrong, not your bot. An unsupported Node runtime, an optional native module the image cannot load, two packages that must move in lockstep and did not, an executable the audio path expected on PATH.
string
One actionable paragraph naming what to change on the machine. Nothing about your bot’s code, token, or permissions is at fault, so print this next to message.
login() raises one on the first connect for a failure it recognizes, including an unsupported Node version. Reconnects keep their normal backoff and log the remedy under debug. The voice audio path raises the same class from player.play() and ytdlpResource(). See Troubleshooting.

CloakRestError

An HTTP leg of the REST lane failed: cloak-rest-api answered non-2xx, or the request never left the host.
number
The HTTP status, or 0 for a transport failure such as DNS, TLS, or a timeout.
string
The request path with the query string stripped, because ids ride query strings.
string | undefined
The service’s machine-readable error code when the body carried one, for example file_too_large. Surfaced verbatim: the SDK hardcodes no server-side limit.
No credential material is ever quoted into this error, in message, in a field, or in toString().
The credential mint is not an HTTP failure and rejects with CloakActionError instead. The most likely first error anyone sees from client.rest is code -7: bot REST credentials are disabled server-side, which is the shipping default. Nothing is wrong with your bot. An operator has to enable the feature.

CloakEmbedError

A card sendCard() refused, thrown locally before anything reaches the wire. This class exists because the server cannot read an encrypted card and the renderer strips bad values silently, so this is the only place you can be told.
CloakEmbedErrorReason
One of 'shape', 'content', 'cap', 'media-host', 'empty', or 'wire-cap'.
string | undefined
The exact offending path, for example 'embed.image.url'. Absent when the whole payload is at fault.
'media-host' is the one you are most likely to hit: card media must be Cloak-hosted, and there is deliberately no flag that widens it. See Rich cards.

CloakWebhookError

A webhook POST failed, from either webhooks.post() or sendEmbed().
number
The HTTP status, or 0 for a transport failure or a local precheck.
string
The ingress’ own label, such as 'unauthorized', 'rate limited', or 'embeds too large'. When status is 0, an SDK-side label such as 'transport' or 'precheck'.
string | undefined
The webhook this post targeted, when known. Never the secret.
The webhook url never appears in the error, because it embeds the token secret.

Login failures

Login rejections do not throw from a call you can await once the reconnect loop owns the connection. They emit disconnect. Two are terminal and stop the loop, because retrying cannot fix either: -8 is a transient backend error and keeps retrying with backoff. If your bot goes quiet and no reconnecting follows a disconnect, you are looking at -2 or -12. See Connection lifecycle.

describeActionError

An exported helper that maps a raw (opcode, code) pair to the same message and optional permission that CloakActionError uses. Unknown codes get a generic message. You rarely need it, since a thrown error already carries both. It is there for tooling that inspects raw failures.

Next

Handling errors

The catch-and-report pattern in practice.

Permissions

What the permission field points at.

Events

The error and sendRejected events.

Troubleshooting

Environment failures and their remedies.