Discord.JS bot Starboard using two differents emojis and count

# Discord.js Starboard Bot: Dual-Emoji Setup Challenges and Solutions

## Summary
**Discord's starboard** traditionally monitors a single emoji to highlight popular messages. This article explores implementing a **dual-emoji starboard** (🎁 and 🪑) requiring distinct reaction thresholds (6 and 7). While initially intuitive, developers face critical **logic gaps** when handling multiple emojis simultaneously. We'll dissect a real-world implementation flaw and provide production-ready solutions.

## Root Cause
The core issue stems from **misinterpreting event handling** for Discord.js reactions. The original code attempts to validate BOTH emojis' thresholds during a SINGLE reaction event—an impossible scenario since each event concerns only one emoji. 

The problematic condition:
```javascript
if ((emoji.name === CONFIG.MPREG.EMOJI && count == CONFIG.MPREG.LIMIT) 
    && 
    (emoji.name === CONFIG.CHAIR.EMOJI && count == CONFIG.CHAIR.LIMIT)) {
    // ...
}

Crucially: A reaction event fires per-emoji when users react. At runtime:

  • A 🪑-reaction triggers with only 🪑 metrics
  • A 🫃-reaction triggers with only 🫃 metrics
    The && condition can NEVER be satisfiedemoji.name cannot be two values simultaneously.

Why This Happens in Real Systems

  1. Event-driven misunderstandings: Junior developers often conflate:

    • State-based checks (“Are both thresholds met?”)
    • Event-based triggers (“This specific reaction just changed”)
  2. Partial state visibility:
    Events report only changed emoji counts, not all reactions concurrently.

  3. Threshold synchronization:
    Systems requiring compound conditions (Emoji A AND Emoji B) necessitate state persistence between events.

Real-World Impact

  • 🚫 Starboard never triggers despite user reactions
  • Unmet user expectations for “feature completeness”
  • Developer friction debugging async interactions
  • Critical path failure: Core community feature broken

Example Code Revised Implementation

Implement separate emoji checks with complete reaction state lookup. This solution:

  1. Handles emojis individually
  2. Cross-validates total counts using fetched message reactions
  3. Avoids partial event data traps
module.exports = async (reaction) => {
    if (reaction.partial) await reaction.fetch().catch(() => { return; });
    if (reaction.message.author.bot) return;

    const { emoji, message } = reaction;
    const botClient = message.client;

    // Fetch FULL reaction state regardless of event hoisting issues
    await message.fetch().catch(console.error); 

    // Find counts for BOTH target emojis
    const mpregReaction = message.reactions.cache.get(CONFIG.MPREG.EMOJI);
    const chairReaction = message.reactions.cache.get(CONFIG.CHAIR.EMOJI);

    const mpregCount = mpregReaction?.count || 0;
    const chairCount = chairReaction?.count || 0;

    // Separate logic per emoji-and-threshold pair
    const isMpregTrigger = emoji.name === CONFIG.MPREG.EMOJI 
        && mpregCount === CONFIG.MPREG.LIMIT;

    const isChairTrigger = emoji.name === CONFIG.CHAIR.EMOJI 
        && chairCount === CONFIG.CHAIR.LIMIT;

    // Only activate on ANY threshold hit
    if (isMpregTrigger || isChairTrigger) {
        const mpregchairChannel = botClient.channels.cache.get(CONFIG.MPREG.ID);
        if (!mpregchairChannel) return;

        const embed = new EmbedBuilder()
            .setColor(0xEDDC24)
            .setAuthor({ name: message.author.tag, iconURL: message.author.displayAvatarURL() })
            .setDescription(message.content || " [No text content] ")
            .addFields({ name: 'Channel', value: `${message.channel}`, inline: true })
            .setFooter({ text: `Message ID: ${message.id}` })
            .setTimestamp();

        if (message.attachments.size > 0) {
            embed.setImage(message.attachments.first().proxyURL);
        }

        // Notify with combined content
        await mpregchairChannel.send({ 
            content: `🫃🪑 | ${message.url}`, 
            embeds:  
        });
    }
};

How Senior Engineers Fix It

  1. Decouple emoji handling:
    Treat reactions as independent events but validate against global state.

  2. Fetch complete message state:
    Use message.fetch() post-reaction to overcome partial data limitations.

  3. Separate triggers:
    Check thresholds per-emoji then OR-combine (||) triggers—not AND (&&).

  4. Defensive access:
    Safeguard against missing reactions with optional chaining (?.).

  5. Idempotency guards:
    Track message IDs to prevent duplicate starboard posts if bots restart.

  6. Configuration validation:
    Add startup checks to verify .env IDs exist in guild context.

Why Juniors Miss It

  • Event-driven blind spots: Assuming reaction events expose all reactions
  • Synchronous thinking: Expecting “at this moment” state represents persistent truth
  • Incomplete docs: Discord.js doesn’t highlight partial reaction caveats
  • Boolean logic pitfalls: Misapplying compound conditions across async events
  • Testing gaps: Local trials often use single-emojis/accounts hiding dual-emoji bugs

Key Principle: Treat Discord reaction events as incremental state changes—not total state snapshots. Always fetch full message context before validation.

Leave a Comment