How to create Text Channels and Threads in Discord.JS V14

2/2/2023, 3:27:17 PM

This is the first (of four) parts of the Discord.JS V14 tutorial that I've published. You can also read the full version of this tutorial on Medium if you prefer.

Navigating through Discord.JS' guide and Discord.JS' guide can be confusing if you are new to programming, as many bot creators will also be learning how to code to create their bot (it was my first project too, back in 2018!). This post aims to simplify the learning process to make different types of Channel from a slash command interaction. Before anything else, it's essential to share links to relevant resources used in this blog post:

  1. Discord.JS' support server
  2. Discord.JS' guide (currently at v14)
  3. Discord.JS' documentation (currently at v14)

Remember that if you have any questions, Google your issue first, and, if you can't find the solution, join the support server and ask for help in the proper v14 help channel.

First, this guide assumes you already have a bot set up, use slash command interactions, you have a command handler set up and the command you are using was already deployed. You will need to update the code on your own if you are still using messageCreate events instead.

Second, this guide assumes you are using Javascript over Typescript. Feel free to use the documentation website if you need assistance defining your types.

Third, all code for the topics in these lessons will be available on this GitHub repository. The code written is very verbose on purpose, as this guide aims to help even the newest programmer to update their bot. You will see values being checked for true and then for false right after. This is intended because I want to make the code examples extremely clear and easy to understand for newer programmers. If you choose to copy my files and you are more experienced in writing Javascript, feel free to update the code and remove those additional checks.

Last, but not least, this article will briefly touch on the creation of certain Discord features, but will not dive deep into topics like "editing an existing channel" or "setting permissions for channels". These topics can be used to write new guides in the future if the interest is there.

Creating your first Text Channel

Let's start with your basic file. We will not have options added as I'm not assuming how configurable you want the commands to be, but you can (and we will) add more options later.

// Importing SlashCommandBuilder is required for every slash command // We import PermissionFlagsBits so we can restrict this command usage // We also import ChannelType to define what kind of Channel we are creating const { SlashCommandBuilder, PermissionFlagsBits, ChannelType } = require( "discord.js", ); module.exports = { data: new SlashCommandBuilder() .setName("createnewchannel") // Command name matching file name .setDescription('Ths command creates a text channel called "new"') // You will usually only want users that can create new Channels to // be able to use this command and this is what this line does. // Feel free to remove it if you want to allow any users to // create new Channels .setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels) // It's impossible to create normal Text Channels inside DMs, so // it's in your best interest in disabling this command through DMs // as well. Threads, however, can be created in DMs, but we will see // more about them later in this post .setDMPermission(false), async execute(interaction) { // Before executing any other code, we need to acknowledge the interaction. // Discord only gives us 3 seconds to acknowledge an interaction before // the interaction gets voided and can't be used anymore. await interaction.reply({ content: "Fetched all input and working on your request!", }); try { // Now create the Channel in the server. await interaction.guild.channels.create({ name: "new", // The name given to the Channel type: ChannelType.GuildText, // The type of the Channel created. // Since "text" is the default Channel created, this could be ommitted }); // Notice how we are creating a Channel in the list of Channels // of the server. This will cause the Channel to spawn at the top // of the Channels list, without belonging to any Categories (more on that later) // If we managed to create the Channel, edit the initial response with // a success message await interaction.editReply({ content: "Your channel was successfully created!", }); } catch (error) { // If an error occurred and we were not able to create the Channel // the bot is most likely received the "Missing Permissions" error. // Log the error to the console console.log(error); // Also inform the user that an error occurred and give them feedback // about how to avoid this error if they want to try again await interaction.editReply({ content: "Your channel could not be created! Please check if the bot has the necessary permissions!", }); } }, };
View/Download file

Deploying this command, refreshing your client, and triggering the command will always create a new Channel called "new". But that's not very versatile, is it? Let's tweak the code a bit so we can choose what name we want to give the Channel now.

Creating a Text Channel with a dynamic name

To be able to set the name from the command itself, we need to tweak the code a little, add options to the command, and then retrieve the name provided by the user in our file.

// ... // initial code unchanged // ... // Text Channel name .addStringOption((option) => option .setName('channelname') // option names need to always be lowercase and have no spaces .setDescription('Choose the name to give to the channel') .setMinLength(1) // A Text Channel needs to be named .setMaxLength(25) // Discord will cut-off names past the 25 characters, // so that's a good hard limit to set. You can manually increase this if you wish .setRequired(true) ) // ... // code in between unchanged // ... const chosenChannelName = interaction.options.getString('channelname'); // ... // code in between unchanged // ... await interaction.guild.channels.create({ name: chosenChannelName, // The name given to the Channel by the user type: ChannelType.GuildText, // The type of the Channel created. // Since "text" is the default Channel created, this could be ommitted }); // ... // rest of the code unchanged // ...
View/Download file

A small note here: Discord has a visual limit of around 25 characters for channels on the sidebar, but they don't define an actual hard limit for Channel names and expect users to have common sense. If you choose to override the 25-character limit in line 15, please ensure there is another limit in place to stop users from creating absurdly long Channel names. Avoid giving them enough rope to hang themselves.

After saving your file, reloading your bot, and redeploying your commands, you now have a command that can quickly create a new Text Channel. Neat, huh? But we are still creating stray Channels at the top of the Channel list. Let's change that now.

Create a Channel that is nested in the parent Category (when there is one)

More experienced Discord users won't be expecting the newly created Channel to appear on the top of the Channel list, but instead, to be within the same Category of the Channel they used the command in. We will now be tweaking our code to create Channels with that behavior whenever the server has Categories set up, but still, be able to create stray Channels if the command was used in a Channel that isn't nested in a Category.

// ... // initial code unchanged // ... // Check if this Channel where the command was used is stray if (!interaction.channel.parent) { // If the Channel where the command was used is stray, // create another stray Channel in the server. await interaction.guild.channels.create({ name: chosenChannelName, // The name given to the Channel by the user type: ChannelType.GuildText, // The type of the Channel created. // Since "text" is the default Channel created, this could be ommitted }); // Notice how we are creating a Channel in the list of Channels // of the server. This will cause the Channel to spawn at the top // of the Channels list, without belonging to any Categories (more on that later) // If we managed to create the Channel, edit the initial response with // a success message await interaction.editReply({ content: 'Your channel was successfully created!', }); return; } // Check if this Channel where the command was used belongs to a Category if (interaction.channel.parent) { // If the Channel where the command belongs to a Category, // create another Channel in the same Category. await interaction.channel.parent.children.create({ name: chosenChannelName, // The name given to the Channel by the user type: ChannelType.GuildText, // The type of the Channel created. // Since "text" is the default Channel created, this could be ommitted }); // If we managed to create the Channel, edit the initial response with // a success message await interaction.editReply({ content: 'Your channel was successfully created in the same category!', }); return; } // ... // rest of the code unchanged // ...
View/Download file

Let's go through this bit by bit.

if (!interaction.channel.parent) { (line 45)

First, we are checking if the Channel where the command was used doesn't have a parent, which would mean they are not nested within a Category. If this check succeeds, then this is a stray Channel and we can reuse our previous code to create another stray Channel . Do note that we end this if check with a return statement. More on that is below.

if (interaction.channel.parent) { (line 66)

Now we are checking if the Channel where the command was used does have a parent, meaning they are nested in a Category.

await interaction.channel.parent.children.create({ (line 69)

Since the Channel where the command was used does have a parent, we need to create our Channel inside that same Category. For that, we need to first access the children of that Category and then create our Channel within.

Now that we are handling both cases for stray Channels and nested Channels, let's switch gears for a moment and talk about Thread before we proceed to Voice Channels, Categories, and Roles.

Create a Thread with a dynamic name

Starting a Thread is about as simple as creating a Channel, all you need is either a message or a Channel that will host the Thread. Because Threads live inside a Channel, regardless of using a message as a starting point or not, you don't need to care for Categories.

// Importing SlashCommandBuilder is required for every slash command const { SlashCommandBuilder } = require("discord.js"); module.exports = { data: new SlashCommandBuilder() .setName("createthread") // Command name matching file name .setDescription("Creates a new thread") // Thread name .addStringOption((option) => option .setName("threadname") // option names need to always be lowercase and have no spaces .setDescription("Choose the name to give to the thread") .setRequired(true) ) // Thread starts from message .addBooleanOption((option) => option .setName("messageparent") // option names need to always be lowercase and have no spaces .setDescription( "Choose if this thread should be use the initial message as parent", ) .setRequired(true) ), async execute(interaction) { // Before executing any other code, we need to acknowledge the interaction. // Discord only gives us 3 seconds to acknowledge an interaction before // the interaction gets voided and can't be used anymore. const interactionReplied = await interaction.reply({ content: "Fetched all input and working on your request!", fetchReply: true, // notice how we are instantiating this reply to // a constant and passing `fetchReply: true` in the reply options }); // After acknowledging the interaction, we retrieve the string sent by the user const chosenThreadName = interaction.options.getString("threadname"); const threadWithMessageAsParent = interaction.options.getBoolean( "messageparent", ); // Do note that the string passed to the method .getString() and .getBoolean() // needs to match EXACTLY the name of the options provided (line 10 and 17 // in this file). If it's not a perfect match, these will always return null try { // Check if the current Channel the command was used is a Thread, // which would cause the creation of another Thread to fail. // Threads cannot be parents of other Threads! if (interaction.channel.isThread() == true) { // If the current Channel is a Thread, return a fail message // and stop the command await interactionReplied.edit({ content: `It's impossible to create a thread within another thread. Try again inside a text channel!`, }); return; // Return statement here to stop all further code execution on this file } // Check if the Channel where the command was used is not a Thread if (interaction.channel.isThread() == false) { // If the Channel isn't a Thread, check if the user // requested the initial message to be the parent of the Thread if (threadWithMessageAsParent == true) { // If the initial message will be used as parent for the // Thread, check if the message already has a Thread if (interactionReplied.hasThread == true) { // If the initial message already has a Thread, // return an error message to the user and stop the command interactionReplied.edit({ content: "It was not possible to create a thread in this message because it already has one.", }); return; // Return statement here to stop all further code execution on this file } // If the initial message will be used as parent for the // Thread, check if the message already doesn't have a Thread if (interactionReplied.hasThread == false) { // If the initial message doesn't have a Thread, // create one. await interactionReplied.startThread({ name: chosenThreadName, }); // We don't return here because we want // to use the success message below } } // If the initial message isn't meant to be the parent of // the Thread, create an orphaned Thread in this Channel if (threadWithMessageAsParent == false) { // Create the orphaned Thread in the command Channel await interaction.channel.threads.create({ name: chosenThreadName, }); // We don't return here because we want // to use the success message below } // If we managed to create the Thread, orphaned or using the // initial message as parent, edit the initial response // with a success message await interactionReplied.edit({ content: "Your thread was successfully created!", }); } } catch (error) { // If an error occurred and we were not able to create the Thread, // the bot is most likely received the "Missing Permissions" error. // Log the error to the console console.log(error); // Also inform the user that an error occurred and give them feedback // about how to avoid this error if they want to try again await interactionReplied.edit({ content: "Your thread could not be created! Please check if the bot has the necessary permissions!", }); return; } }, };
View/Download file

Alright, few key points with this code:

  1. We are now storing the message sent as a reply to the interaction in the constant interactionReplied (line 25). This is done so we can refer to that message later if the user requested to use our message as a parent to the Thread we are going to create (line 73).

  2. We also handle the case that the user might not want to create a Thread on the message itself, so an orphaned Thread is created in the Channel where the command was used instead.

Next post: .

Written with 💞 by TheYuriG