> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cloak.chat/llms.txt
> Use this file to discover all available pages before exploring further.

# Reactions

> Add and remove reactions on server messages, and listen for reaction changes.

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.

<Warning>
  Reactions do not work on direct messages. `react()` and `unreact()` reject with a plain `Error` on any message where `msg.isDM` is true, regardless of permissions, before a frame is built. Direct messages arrive on the same `messageCreate` event as server messages, so guard every handler.
</Warning>

## 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.

```ts theme={null}
client.on('messageCreate', (msg) => {
  if (msg.authorId === client.user?.id) return; // ignore our own messages
  if (msg.isDM || !msg.serverId) return; // reactions are server-only
  if (msg.content.trim() !== '!celebrate') return;
  if (!client.can('reaction_add', msg.serverId)) return; // advisory pre-gate
  msg.react('🎉').catch((e) => console.error('react failed', e));
});
```

The `if (msg.isDM || !msg.serverId) return;` guard does two jobs. It keeps `react()` away from DM messages, and it narrows `msg.serverId` from `string | null` to `string` so `can()` accepts it. Without it, TypeScript rejects the `can()` call and plain JavaScript silently returns false for a null server.

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.

```ts theme={null}
await msg.react('👀');
// later, once the bot is done looking
await msg.unreact('👀');
```

## 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.

```ts theme={null}
const { emojis, category } = await client.fetchEmojis();
// The shape of emojis and category is server-defined. Treat the catalog
// as data you look an id up in, then hand that id to react().
const partyId = findEmojiId(emojis, 'party_blob');
await msg.react(partyId);
```

<Note>
  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.
</Note>

## 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.

```ts theme={null}
client.on('reactionUpdate', ({ messageId, reactionId, count, userId, serverId, channelId }) => {
  if (channelId !== watchedChannelId) return;
  console.log(`${reactionId} on ${messageId} is now at ${count}`);
});
```

<ParamField path="messageId" type="string">
  The message whose reactions changed.
</ParamField>

<ParamField path="reactionId" type="string">
  The reaction that changed, the same opaque id you pass to `react()`.
</ParamField>

<ParamField path="count" type="number">
  The new total for that reaction on the message. The event does not say whether the change was an add or a remove, only the new count.
</ParamField>

<ParamField path="userId" type="string">
  The user who added or removed the reaction.
</ParamField>

<ParamField path="serverId" type="string">
  The server the message belongs to.
</ParamField>

<ParamField path="channelId" type="string">
  The channel the message belongs to.
</ParamField>

Because `count` is the new total and no add-or-remove flag is included, treat the event as "this reaction now has this many", not "someone just added one".

### 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`.

```ts theme={null}
client.on('reactionUpdate', (evt) => {
  if (evt.userId === client.user?.id) return; // skip our own reactions
  console.log(`someone reacted with ${evt.reactionId}`);
});
```

<Note>
  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.
</Note>

## React to being mentioned

A reaction acknowledges something without adding a message to the channel, which makes it a good pairing with `msg.mentionsMe`.

```ts theme={null}
client.on('messageCreate', (msg) => {
  if (msg.isDM || !msg.serverId) return;
  if (!msg.mentionsMe) return;
  if (!client.can('reaction_add', msg.serverId)) return;
  msg.react('👍').catch((e) => console.error('react failed', e));
});
```

See [Mentions and replies](/guides/mentions-and-replies) for what `mentionsMe` does and does not cover.

## Handling errors

Three different failures are worth telling apart.

<AccordionGroup>
  <Accordion title="Plain Error, before the wire">
    `react()` and `unreact()` reject with a plain `Error` in two cases. On a direct message, unconditionally. And when the message has no `createdAt`, which the server needs to locate the row. Live messages from `messageCreate` and messages loaded with `fetchMessages` both carry `createdAt`, so in practice only the DM case comes up.
  </Accordion>

  <Accordion title="CloakActionError, from the server">
    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.

    | Code  | Meaning                                  |
    | ----- | ---------------------------------------- |
    | `-1`  | Not authenticated                        |
    | `-2`  | Server error while updating the reaction |
    | `-3`  | No reaction to remove on that message    |
    | `-4`  | Reaction limit reached for that message  |
    | `-5`  | Message or reaction not found            |
    | `-6`  | Already reacted with that emoji          |
    | `-7`  | No existing reaction from you to remove  |
    | `-8`  | Missing `reaction_add`                   |
    | `-9`  | Rate limited (bot reaction throttle)     |
    | `-10` | The bot is timed out in this server      |
  </Accordion>

  <Accordion title="A timeout on an older server">
    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](/production/backend-requirements).
  </Accordion>
</AccordionGroup>

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.

```ts theme={null}
import { CloakActionError } from '@cloak-software/bot-sdk';

try {
  await msg.react('👍');
} catch (err) {
  if (err instanceof CloakActionError) {
    console.error(`the server refused the reaction (${err.code}): ${err.message}`);
    if (err.permission) console.error(`missing ${err.permission}`);
  } else {
    console.error('could not react', err);
  }
}
```

## Next

<CardGroup cols={2}>
  <Card title="Editing, deleting, and pinning" icon="pen" href="/guides/editing-and-pinning">
    The other four message actions, under the same DM rule.
  </Card>

  <Card title="The Message object" icon="message" href="/api-reference/types#message">
    Every field and method a received message carries.
  </Card>

  <Card title="Handling errors" icon="triangle-exclamation" href="/guides/handling-errors">
    Working with `CloakActionError` and friends.
  </Card>

  <Card title="Permissions" icon="key" href="/concepts/permissions">
    Why `reaction_add` must be granted first.
  </Card>
</CardGroup>
