How to implement music queue system for Discord bot audio playback

I’m working on a Discord bot that can search and play music from YouTube. The basic functionality works fine - it searches YouTube and plays the first result. But I’m having trouble setting up a proper queue system where songs play automatically one after another.

My current approach tries to use the ‘speaking’ event to detect when audio finishes, but this doesn’t work reliably since the voice channel reference isn’t available at startup. Also, users can switch channels with commands, which breaks this setup.

const YOUTUBE_API = "<api_key>";

var discordjs = require("discord.js");
var youtubedl = require('ytdl-core');
var httpRequest = require('superagent');

var client = new discordjs.Client();
var currentVoiceChannel = null;
var musicQueue = [];

client.on('ready', function() {
    console.log('Bot is online');
});

client.on('message', function(msg) {
    var args = msg.content.split(' ');
    var cmd = args[0].toLowerCase();
    var params = args.slice(1);

    switch (cmd) {
        case "hello":
            msg.reply("Hi there!");
            break;
        case "!connect":
            ConnectToChannel(params[0], msg);
            break;
        case "!queue":
            AddToQueue(params.join(" "), msg);
            break;
    }
});

function ConnectToChannel(channelName) {
    if (currentVoiceChannel) {
        currentVoiceChannel.disconnect();
    }
    
    var targetChannel = FindChannelByName(channelName);
    return targetChannel.join();
}

function AddToQueue(searchQuery) {
    SearchYoutube(searchQuery);
}

function SearchYoutube(keywords) {
    var apiUrl = 'https://www.googleapis.com/youtube/v3/search' + `?part=snippet&q=${escape(keywords)}&key=${YOUTUBE_API}`;

    httpRequest(apiUrl, (err, res) => {
        if (!err && res.statusCode == 200) {
            var data = res.body;
            
            for (var result of data.items) {
                if (result.id.kind === 'youtube#video') {
                    AddSongToQueue(result.id.videoId);
                    break;
                }
            }
        }
    });
}

function AddSongToQueue(videoId) {
    var videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
    musicQueue.push(videoUrl);
    
    if (musicQueue.length === 1) {
        PlayNextSong();
    }
}

function PlayNextSong() {
    if (musicQueue.length === 0) return;
    
    var nextSong = musicQueue[0];
    var audioStream = youtubedl(nextSong, {filter: 'audioonly'});
    var dispatcher = client.voiceConnections.first().playStream(audioStream);
    
    dispatcher.on('end', () => {
        musicQueue.shift();
        PlayNextSong();
    });
}

What’s the best way to handle automatic queue playback when songs finish? Should I be using different events or a completely different approach?

Looking at your code, the core problem is that you’re not properly managing the dispatcher state and voice connection lifecycle. I ran into similar issues when I built my music bot and found that tracking the current dispatcher separately from the voice connection solved most reliability problems. Create a global variable to store the active dispatcher and always check if it exists before creating a new one. Your PlayNextSong function should also validate that the voice connection is still active before attempting playback. Another crucial fix is handling the dispatcher ‘error’ event alongside ‘end’ - network timeouts and stream failures will break your queue if not caught properly. The voice connection can become stale when users move the bot between channels, so implement a reconnection mechanism within your playback logic. I also suggest adding a playing state flag to prevent multiple songs from starting simultaneously if users spam the queue command. Discord.js voice connections are notoriously finicky, so always include connection state validation before stream operations.

The main issue with your current setup is relying on client.voiceConnections.first() which can be unreliable when users switch channels. I faced this exact problem when building my bot last year. Store the voice connection reference directly when joining a channel instead of trying to retrieve it later. Modify your ConnectToChannel function to return and store the connection object, then pass it to your playback functions. Also, the ‘end’ event can fire unexpectedly due to network issues or user actions. I recommend adding error handling and using ‘finish’ event as a backup. Sometimes Discord’s voice connection drops without proper cleanup, so implement a connection state check before attempting playback. One more thing - consider adding a small delay between songs (around 500ms) to prevent audio overlap issues that can occur with rapid transitions in Discord’s voice system.