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, editCard, in a server channel or (from wire version 3) a DM.
  • Forums: createForumPost, fetchForumPosts, fetchForumMeta, editForumPost, setForumPostState, followForumPost. See Forum posts.
  • Interactions: Interaction.ack, showModal, and ephemeral, when the server refuses the response. See Interaction responses.
  • 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 and sendCardDM, 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 43 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.

Forum posts

The forum methods reject with negative codes. None of them maps to a named permission, because each verdict folds more than one server-side check, so permission is always undefined here and message is what to show. The capabilities the messages name resolve to ordinary permissions: creating a post is message_send on the forum, and managing posts is message_manage. See Forums.
Issued by the SDK before any action that takes postId.
The SDK has no method for this. The table exists so a raw-frame deny still reads as itself.
reactToStarter() is fire-and-forget on the wire and a denial produces no frame, so it never rejects with one of these.

Interaction responses

Interaction.ack(), showModal(), and ephemeral() reject with these when the server refuses the response. The one you are most likely to see is -3: the click was more than 15 minutes ago. See Buttons and selects.
This is the clicker’s opcode. A bot session is refused with -2 before the frame is read, so the SDK never sends it. The table exists so a raw-frame author reading [794, -2] learns why.
A second response to the same interaction never reaches the server. It is refused locally with a CloakClientError whose source is 'interaction'.

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, attribution, frame }, with the same message and permission semantics as CloakActionError.
The deny frame carries no routing, so serverId and channelId come from the SDK’s queue of sends still awaiting their echo, and attribution ('exact', 'ambiguous', or 'none') says how much to trust them.sendRejected is not an acknowledgement channel. Silence does not mean delivered. A send made with ack: true gets a promise result instead: the persisted Message, a CloakActionError for a denial pinned to it, or a CloakSendTimeoutError below.
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. The one exception is source: 'interaction', which is a rejected promise: the second of two responses to the same interaction.
CloakErrorSource
Where it came from: 'listener', 'frame', 'keys', 'transport', 'reconnect', or 'interaction'.
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.

CloakSendTimeoutError

A send made with ack: true got neither its echo nor a deny inside ackTimeoutMs, the socket dropped while it waited, or the only deny that arrived could have belonged to another outstanding send. The outcome is indeterminate: the server may have persisted the message, or it may not, and nothing on the wire says which.
This is not a failure verdict. A retry can duplicate the message; not retrying can lose it. Read history when you must know. Distinct from CloakActionError, which is a definite denial pinned to exactly that send. See Sending is fire-and-forget unless you ask for a receipt.

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.

CloakComponentError

A component row, a modal definition, or an ephemeral reply’s rows were refused, thrown locally before anything reaches the wire. Raised by sendCard(), editCard(), createForumPost(), Interaction.ephemeral(), and Interaction.showModal(), and by assertComponents() and assertModal() when you call them yourself.
CloakComponentErrorReason
'shape' for a wrong type, an unknown key, a missing required key, or a duplicate id. 'cap' for a value over one of the pinned caps.
string | undefined
The offending path, for example 'components[1].components[0].label'.
Every cap is listed under Component constants. The client parser enforces the same caps and drops a card whole on a violation, which is why the SDK refuses it first. See Buttons and selects.

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.