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.
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, andephemeral, 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, andfetchMessageswhen read history is denied. - Direct messages:
sendDMandsendCardDM, on the create and key-exchange legs. An ineligible target is refused here. - Presence:
setStatus. - Voice:
joinVoiceandleaveVoice. - Commands:
sendCommand. - Webhook CRUD:
webhooks.list,.create,.edit,.regenerate,.delete. - REST:
client.rest.*, when the credential mint is refused.
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, sopermission 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.
createForumPost (opcode 750)
createForumPost (opcode 750)
Entering a post (opcode 751)
Entering a post (opcode 751)
Issued by the SDK before any action that takes
postId.fetchForumPosts (opcode 752)
fetchForumPosts (opcode 752)
editForumPost (opcode 754)
editForumPost (opcode 754)
setForumPostState (opcode 755)
setForumPostState (opcode 755)
followForumPost (opcode 756)
followForumPost (opcode 756)
fetchForumMeta (opcode 757)
fetchForumMeta (opcode 757)
Updating forum settings (opcode 758, raw lane only)
Updating forum settings (opcode 758, raw lane only)
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.
Responding to an interaction (opcode 796)
Responding to an interaction (opcode 796)
Sending a click (opcode 794, never sent by the SDK)
Sending a click (opcode 794, never sent by the SDK)
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.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.
{ code, message, permission?, serverId, channelId, attribution, frame }, with the same message and permission semantics as CloakActionError.
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 theerror 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.
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 onPATH.
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 withack: 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.
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.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 cardsendCard() 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 bysendCard(), 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'.CloakWebhookError
A webhook POST failed, from eitherwebhooks.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.
Login failures
Login rejections do not throw from a call you can await once the reconnect loop owns the connection. They emitdisconnect.
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
(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.