Skip to main content
Your bot can react to any server message it can see, remove its own reaction, and hear when reactions change anywhere it is watching. Reactions are keyed by an opaque reactionId: a unicode emoji for the common case, or a custom emoji id for server-specific emoji. This page covers both sides, adding reactions and listening for them.
react() and unreact() on a direct message need a backend that honors wire version 3, which is what gives a DM message its createdAt. On an older backend a DM message carries no timestamp and both reject with a plain Error saying so, before a frame is built. Direct messages arrive on the same messageCreate event as server messages, so guard every handler that does server-scoped work. See Direct messages.

Add a reaction

Call msg.react(reactionId) on a Message to add the bot’s reaction. It returns Promise<void> and requires the reaction_add permission. For a plain emoji, pass the unicode character directly.
The if (msg.isDM || !msg.serverId) return; guard narrows msg.serverId from string | null to string so can() accepts it, since a permission grant is a server concept. Without it, TypeScript rejects the can() call and plain JavaScript silently returns false for a null server. A bot that also reacts inside DMs skips can() on that branch and calls msg.react() directly. The reactionId is opaque. The SDK does not validate it and passes it straight through to the server. An invalid id is refused by the server, not caught locally.

Remove the bot’s reaction

Call msg.unreact(reactionId) to remove the bot’s own reaction from a message. It only affects the reaction the bot added, not anyone else’s.

React with a custom emoji

For a server’s custom emoji, pass its id instead of a unicode character. Get ids from client.fetchEmojis(), which returns Promise<{ emojis, category }>, the global emoji catalog. Its ids are exactly the custom-emoji ids that react() accepts.
The shape of emojis and category is defined by the server, so treat them as data to look ids up in rather than a fixed structure.

Listen for reaction changes

The reactionUpdate event fires when a reaction is added or removed on a message your bot can see. It is not scoped to a channel you selected: on a current Cloak server it arrives for every server the bot is in, the same way messageCreate does. Filter on channelId if you care about one place.
string
The message whose reactions changed.
string
The reaction that changed, the same opaque id you pass to react().
number
The new total for that reaction on the message. The wire carries only this total, never an add-or-remove flag.
'add' | 'remove' | 'unknown'
Inferred by comparing count with previousCount. 'unknown' when the SDK has no earlier count for this message and reaction (the first hook for it since the bot started, or an evicted entry) or when the total did not change.
number | null
The last total this session saw for the same message and reaction, or null.
string
The user who added or removed the reaction.
string
The server the message belongs to.
string
The channel the message belongs to.
Because count is the new total and the wire includes no add-or-remove flag, operation is only as good as the SDK’s memory of the previous total. It is exact for any reaction the bot has watched since it started; the first hook for a reaction after a restart reads 'unknown'. Treat the event as “this reaction now has this many”, and use operation when a direction is needed and 'unknown' is acceptable.

Filter out the bot’s own reactions

reactionUpdate fires for the bot’s own reactions too, including the ones it adds with react(). When that matters, filter on userId.
On an older Cloak server, serverId and channelId on reactionUpdate can fall back to the bot’s currently selected server and channel. An up-to-date Cloak server always reports the message’s own location.

React to being mentioned

A reaction acknowledges something without adding a message to the channel, which makes it a good pairing with msg.mentionsMe.
See Mentions and replies for what mentionsMe does and does not cover.

Handling errors

Three different failures are worth telling apart.
react() and unreact() reject with a plain Error when the message has no createdAt, which the server needs to locate the row. Live server messages and messages loaded with fetchMessages always carry it. A DM message carries it only on a backend that honors wire version 3, so in practice the case that comes up is a DM on an older backend.
Both methods reject with CloakActionError when the server refuses. Read .code for the reason and .permission for the missing permission when the code maps to one.
Confirming a reaction needs a Cloak server new enough to send the bot a success acknowledgement. Against an older backend the call cannot confirm and times out even though the reaction landed. See What the server must support.
Pre-gate with can('reaction_add', serverId) to skip actions the server would only refuse, and still catch the promise for the cases you cannot predict.

Next

Editing, deleting, and pinning

The other four message actions, under the same DM rule.

The Message object

Every field and method a received message carries.

Handling errors

Working with CloakActionError and friends.

Permissions

Why reaction_add must be granted first.