Receiving "ShardingReadyTimeout" even though ready event is firing

Hi there! I'm dealing with a strange problem and I'm not sure how to handle it. I'm receiving a ShardingReadyTimeout error, but the ready event is being fired before the error is occuring, so I therefore not sure how to handle it. I'm pretty sure it's being caused by my event handler, but I'm not sure why or how. Essentially, when the bot is actually created, it's firing a loadEvents method which is loading all the event files within my event directory, and attaching them to the client object. An event has a type of:
export type Event = {
name: keyof RestEvents | string
once: boolean
rest: boolean
execute: (...args: any[]) => void | Promise<void>
}
export type Event = {
name: keyof RestEvents | string
once: boolean
rest: boolean
execute: (...args: any[]) => void | Promise<void>
}
My index.ts, which is the sharding manager:
import { ShardingManager } from "discord.js"
import { getConfig } from "./modules/config"

const config = getConfig()
const manager = new ShardingManager("./bot.js", { totalShards: config.shards, token: `${config.token}` })

manager.on("shardCreate", shard => {
console.log(`Launched shard ${shard.id}`)
})

process.on("unhandledRejection", err => {
console.log((err as Error).stack, "error")
})

void manager.spawn()
import { ShardingManager } from "discord.js"
import { getConfig } from "./modules/config"

const config = getConfig()
const manager = new ShardingManager("./bot.js", { totalShards: config.shards, token: `${config.token}` })

manager.on("shardCreate", shard => {
console.log(`Launched shard ${shard.id}`)
})

process.on("unhandledRejection", err => {
console.log((err as Error).stack, "error")
})

void manager.spawn()
My client.ts, which is the actual bot code:
import { GatewayIntentBits, Partials } from "discord.js"
import { getConfig } from "./modules/config"
import CrystalClient from "./types/CrystalClient"

const config = getConfig()
const client = new CrystalClient({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildVoiceStates,
],
partials: [Partials.User, Partials.Message, Partials.GuildMember, Partials.ThreadMember],
})

void client.login(config.token)
import { GatewayIntentBits, Partials } from "discord.js"
import { getConfig } from "./modules/config"
import CrystalClient from "./types/CrystalClient"

const config = getConfig()
const client = new CrystalClient({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildVoiceStates,
],
partials: [Partials.User, Partials.Message, Partials.GuildMember, Partials.ThreadMember],
})

void client.login(config.token)
I'm extending the discord.js Client object to support some of my own functionality, so here is that file:
import { Client, type ClientOptions, Collection, type GuildBasedChannel } from "discord.js"
import { loadEvents } from "../modules/utils/eventHandler"
import { type Command } from "./Command"

export default class CrystalClient extends Client {
static commands: Collection<string, Command> // Not used yet, can be ignored
static events: Collection<string, (...args: any[]) => void> // Not used yet, can be ignored
static guilds: string[] // Not used yet, can be ignored
static channels: GuildBasedChannel[] = [] // Not used yet, can be ignored
private static client: CrystalClient

constructor(options: ClientOptions) {
super(options)
CrystalClient.client = this

CrystalClient.commands = new Collection() // Not used yet, can be ignored
CrystalClient.events = new Collection() // Not used yet, can be ignored

void loadEvents(CrystalClient.client)
}

static getClient(): CrystalClient {
return CrystalClient.client
}
}
import { Client, type ClientOptions, Collection, type GuildBasedChannel } from "discord.js"
import { loadEvents } from "../modules/utils/eventHandler"
import { type Command } from "./Command"

export default class CrystalClient extends Client {
static commands: Collection<string, Command> // Not used yet, can be ignored
static events: Collection<string, (...args: any[]) => void> // Not used yet, can be ignored
static guilds: string[] // Not used yet, can be ignored
static channels: GuildBasedChannel[] = [] // Not used yet, can be ignored
private static client: CrystalClient

constructor(options: ClientOptions) {
super(options)
CrystalClient.client = this

CrystalClient.commands = new Collection() // Not used yet, can be ignored
CrystalClient.events = new Collection() // Not used yet, can be ignored

void loadEvents(CrystalClient.client)
}

static getClient(): CrystalClient {
return CrystalClient.client
}
}
8 Replies
d.js toolkit
d.js toolkit14mo ago
- What's your exact discord.js npm list discord.js and node node -v version? - Not a discord.js issue? Check out #other-js-ts. - Consider reading #how-to-get-help to improve your question! - Explain what exactly your issue is. - Post the full error stack trace, not just the top part! - Show your code! - Issue solved? Press the button! - Marked as resolved by OP
TerrorByte
TerrorByteOP14mo ago
I reached a character limit, so here's the rest of the info: Here is my eventHandler class, which contains the loadEvents method:
import { type RestEvents } from "discord.js"
import { loadFiles } from "./fileLoader"
import { type Event } from "../../types/Event"
import CrystalClient from "../../types/CrystalClient"

export async function loadEvents(client: CrystalClient): Promise<void> {
console.time("Events loaded")

const events = []
CrystalClient.events.clear()
client.removeAllListeners()

const files = await loadFiles("modules/events")

for (const file of files) {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const event: Event = require(file)
if (event.name == null) throw new Error("Invalid event file")

const execute = (...args: any[]): void => {
void event.execute(...args)
}

event.rest
? client.rest[event.once ? "once" : "on"](event.name as keyof RestEvents, execute)
: client[event.once ? "once" : "on"](event.name, execute)

CrystalClient.events.set(event.name, execute)
events.push({ Event: event.name, Status: "✅" })
} catch (error) {
console.error(error)
events.push({ Event: file.replace(/\\/g, "/").split("/").pop()?.slice(0, -3), Status: "🛑" })
}
}

console.table(events, ["Event", "Status"])
console.info("\n\x1b[36m%s\x1b[0m", "Loaded Events")
console.timeEnd("Events loaded")
}
import { type RestEvents } from "discord.js"
import { loadFiles } from "./fileLoader"
import { type Event } from "../../types/Event"
import CrystalClient from "../../types/CrystalClient"

export async function loadEvents(client: CrystalClient): Promise<void> {
console.time("Events loaded")

const events = []
CrystalClient.events.clear()
client.removeAllListeners()

const files = await loadFiles("modules/events")

for (const file of files) {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const event: Event = require(file)
if (event.name == null) throw new Error("Invalid event file")

const execute = (...args: any[]): void => {
void event.execute(...args)
}

event.rest
? client.rest[event.once ? "once" : "on"](event.name as keyof RestEvents, execute)
: client[event.once ? "once" : "on"](event.name, execute)

CrystalClient.events.set(event.name, execute)
events.push({ Event: event.name, Status: "✅" })
} catch (error) {
console.error(error)
events.push({ Event: file.replace(/\\/g, "/").split("/").pop()?.slice(0, -3), Status: "🛑" })
}
}

console.table(events, ["Event", "Status"])
console.info("\n\x1b[36m%s\x1b[0m", "Loaded Events")
console.timeEnd("Events loaded")
}
The loadFiles() function is defined as
import { glob } from "glob"
import path from "node:path"

async function deleteCachedFile(file: string): Promise<void> {
const filePath = path.resolve(file)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
if (require.cache[filePath] != null) delete require.cache[filePath]
}

export async function loadFiles(dirName: string): Promise<string[]> {
try {
const files = await glob(path.join(process.cwd(), dirName, "**/*.js").replace(/\\/g, "/"))
const jsFiles = files.filter(file => path.extname(file) === ".js")
await Promise.all(jsFiles.map(deleteCachedFile))
return jsFiles
} catch (error: any) {
console.error(`Error loading files from directory ${dirName}: ${error}`)
throw error
}
}
import { glob } from "glob"
import path from "node:path"

async function deleteCachedFile(file: string): Promise<void> {
const filePath = path.resolve(file)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
if (require.cache[filePath] != null) delete require.cache[filePath]
}

export async function loadFiles(dirName: string): Promise<string[]> {
try {
const files = await glob(path.join(process.cwd(), dirName, "**/*.js").replace(/\\/g, "/"))
const jsFiles = files.filter(file => path.extname(file) === ".js")
await Promise.all(jsFiles.map(deleteCachedFile))
return jsFiles
} catch (error: any) {
console.error(`Error loading files from directory ${dirName}: ${error}`)
throw error
}
}
If I change the void loadEvents(CrystalClient.client) line to a standard this.on("ready", () => { console.log("ready") }) method, it works. But when I'm loading events dynamically from an event file, it freaks out. For reference, this event is being registered fine
import { type Event } from "../../types/Event"
import { loadCommands } from "../utils/commandHandler"
import { getChannels } from "../utils/channelRegistrar"
import type CrystalClient from "../../types/CrystalClient"

module.exports = {
name: "ready",
once: false,
rest: false,
execute: async function (client: CrystalClient): Promise<void> {
// void loadCommands(client)
// void getChannels(client)

console.log(`Logged in as ${client.user?.tag}`)
},
} satisfies Event
import { type Event } from "../../types/Event"
import { loadCommands } from "../utils/commandHandler"
import { getChannels } from "../utils/channelRegistrar"
import type CrystalClient from "../../types/CrystalClient"

module.exports = {
name: "ready",
once: false,
rest: false,
execute: async function (client: CrystalClient): Promise<void> {
// void loadCommands(client)
// void getChannels(client)

console.log(`Logged in as ${client.user?.tag}`)
},
} satisfies Event
But the error still fires
Launched shard 0
┌─────────┬─────────┬────────┐
│ (index) │ Event │ Status │
├─────────┼─────────┼────────┤
│ 0 │ 'ready' │ '✅' │
└─────────┴─────────┴────────┘

Loaded Events
Events loaded: 55.416ms
Logged in as <BOT ID HERE> // This is fired from the ready event
//////// After about 30 seconds
Error [ShardingReadyTimeout]: Shard 0's Client took too long to become ready.
at Timeout.onTimeout (V:\Development Projects\crystal-bot\node_modules\.pnpm\[email protected]\node_modules\discord.js\src\sharding\Shard.js:183:16)
at listOnTimeout (node:internal/timers:573:17)
at process.processTimers (node:internal/timers:514:7) error
Launched shard 0
┌─────────┬─────────┬────────┐
│ (index) │ Event │ Status │
├─────────┼─────────┼────────┤
│ 0 │ 'ready' │ '✅' │
└─────────┴─────────┴────────┘

Loaded Events
Events loaded: 55.416ms
Logged in as <BOT ID HERE> // This is fired from the ready event
//////// After about 30 seconds
Error [ShardingReadyTimeout]: Shard 0's Client took too long to become ready.
at Timeout.onTimeout (V:\Development Projects\crystal-bot\node_modules\.pnpm\[email protected]\node_modules\discord.js\src\sharding\Shard.js:183:16)
at listOnTimeout (node:internal/timers:573:17)
at process.processTimers (node:internal/timers:514:7) error
npm list discord.js && node -v:
[email protected] V:\Development Projects\crystal-bot
└── [email protected] -> .\node_modules\.pnpm\[email protected]\node_modules\discord.js
v20.9.0
[email protected] V:\Development Projects\crystal-bot
└── [email protected] -> .\node_modules\.pnpm\[email protected]\node_modules\discord.js
v20.9.0
I know there's a lot there, I can break it down if needed but basically, ready event is being fired, but shard is still timing out and idk why TL;DR 1) Sharding manager spawns a shard 2) The shard creates a client of type CrystalClient, which extends Client 3) The CrystalClient constructor calls the loadEvents method, which A) iterates over all files and filters the files by .js extensions and B) attaches them to the client object using client[event.once ? "once" : "on"](event.name, execute), where event.once and execute is a property on the imported Event and execute is defined as
const execute = (...args: any[]): void => {
void event.execute(...args)
}
const execute = (...args: any[]): void => {
void event.execute(...args)
}
The whole event loading process is completed after approx. 55.416ms according to the last run of my bot 4) One of those events that is loaded is the client's ready event, which fires fine 5) However, after about 30 seconds the bot times out anyway 6) If void loadEvents() is replaced with this.on("ready", () => { console.log("ready") }) within the constructor of CrystalClient, no errors occur, leading me to believe it's something to do with the file loading process. However, again, the ready event is still being thrown as indicated by hardcoded console.log() methods within the ready event file itself
duck
duck14mo ago
do you still have this issue if you don't remove all listeners from your client with client.removeAllListeners() in loadEvents?
TerrorByte
TerrorByteOP14mo ago
Let me give that a shot, I'll report back in a sec Hmm, oddly enough I don't, even after enabling all my bot's features again. Thinking through it, is that call removing the ready event from the client, therefore preventing it from being fired upon initial construction?
TerrorByte
TerrorByteOP14mo ago
Awesome, good to know. I'm assuming this method should be considered unsafe then and not used? At least, not during startup
duck
duck14mo ago
I believe in general it's considered poor practice to remove listeners not set by the same section of code but yes, especially in this case you'll specifically want to avoid indiscriminately removing all listeners
TerrorByte
TerrorByteOP14mo ago
Alright, thank you very much. I'll figure out a way to only remove events created by me and not the client. Much appreciated!
Want results from more Discord servers?
Add your server