Lifecycle
Plugin lifecycle hooks, client timeline, handler loading, and startup events.
The @robojs/discordjs plugin follows a structured lifecycle that creates the Discord client, logs in, registers gateway listeners, and shuts down cleanly. Understanding this lifecycle helps you hook into the right moment for initialization, cleanup, and client access.
Timeline
The plugin lifecycle runs in three phases during Robo's startup:
prepare start clientReady stop
│ │ │ │
├─ Create Client ├─ Validate token ├─ Caches populated ├─ client.destroy()
├─ Set up state ├─ Wait for server* ├─ Check intents ├─ Clear client ref
├─ Load handlers ├─ client.login() ├─ Bot is online │
├─ Register ├─ Register commands* │ │
│ event listeners│ │ │
│ │ │ │* Only in mock mode
Robo lifecycle files (recommended)
The recommended way to hook into startup and shutdown is with Robo lifecycle files. These run for all Robo projects, not just Discord bots.
src/robo/start.ts
Runs during Robo startup, after the plugin's prepare phase. Use it for initial data loading, connecting to external services, or any setup that should happen before the bot goes online.
import { getClient } from '@robojs/discordjs'
import type { StartContext } from 'robo.js'
export default async (context: StartContext) => {
context.logger.info('Bot is starting up...')
// Access the Discord client (created during prepare)
const client = getClient()
client.user?.setPresence({ status: 'dnd' })
// Load data, connect to databases, etc.
await loadInitialData()
}import { getClient } from '@robojs/discordjs'
export default async (context) => {
context.logger.info('Bot is starting up...')
const client = getClient()
client.user?.setPresence({ status: 'dnd' })
await loadInitialData()
}src/robo/stop.ts
Runs during shutdown. Use it for cleanup, disconnecting external services, or saving state.
import type { StopContext } from 'robo.js'
export default async (context: StopContext) => {
context.logger.info(`Shutting down (reason: ${context.reason})...`)
await saveState()
await disconnectDatabase()
}export default async (context) => {
context.logger.info(`Shutting down (reason: ${context.reason})...`)
await saveState()
await disconnectDatabase()
}These are just two of six lifecycle hooks. Robo.js also supports init.ts, prepare.ts, error.ts, and hmr.ts. See Lifecycle Hooks for the complete reference.
The clientReady event
The clientReady event is a Discord.js gateway event that fires when the client's caches are populated and the bot is fully connected. This is different from robo/start.ts -- the clientReady event fires after the client has connected to Discord's gateway and populated its cache.
import type { EventConfig } from '@robojs/discordjs'
import type { Client } from 'discord.js'
export const config: EventConfig = {
frequency: 'once'
}
export default (client: Client) => {
console.log(`Logged in as ${client.user?.tag}`)
console.log(`Serving ${client.guilds.cache.size} servers`)
}export const config = {
frequency: 'once'
}
export default (client) => {
console.log(`Logged in as ${client.user?.tag}`)
console.log(`Serving ${client.guilds.cache.size} servers`)
}Use frequency: 'once' on the clientReady event since it should typically only run on the initial connection. The handler is disabled at runtime after its first execution and re-enables on restart.
robo/start.ts vs clientReady event
robo/start.ts | clientReady event | |
|---|---|---|
| When | During Robo startup | After Discord gateway connection |
| Client state | Client exists but may not be logged in | Client is fully connected, caches populated |
| Use for | General startup logic, data loading | Discord-specific setup that needs cached data |
| Framework | Robo.js (works for all project types) | Discord.js gateway event |
Handler loading
The plugin uses two different loading strategies depending on the environment:
| Mode | Strategy | Reason |
|---|---|---|
| Production | Eager loading | All handlers are imported in parallel during prepare for fastest runtime access |
| Development | Lazy loading | Handlers are imported on-demand for faster startup and HMR support |
| Mock | Lazy loading | Avoids module linking issues with test runners |
This is automatic -- no user configuration is needed. The plugin determines the loading strategy based on Mode.isDev() and the ROBO_MOCK_MODE environment variable.
Client access
Access the Discord client anywhere in your code using getClient() and hasClient():
import { getClient, hasClient } from '@robojs/discordjs'
// Check if client is available
if (hasClient()) {
const client = getClient()
client.user?.setPresence({ status: 'online' })
}import { getClient, hasClient } from '@robojs/discordjs'
if (hasClient()) {
const client = getClient()
client.user?.setPresence({ status: 'online' })
}getClient() throws if called before the prepare hook completes. Use hasClient() to safely check availability, especially in code that may run during early initialization.
Mock mode lifecycle
When running with @robojs/mock (ROBO_MOCK_MODE=true), the lifecycle has a few differences:
- Server wait: The start hook waits for
@robojs/serverto be ready before logging in, since the mock server must be listening before Discord.js can connect to its mock gateway - Runtime registration: Commands are registered at runtime instead of build time, since the build/complete step is skipped in mock mode
- Standalone mode: When
__ROBO_MOCK_STANDALONE=true, the start hook skips login entirely -- the mock server runs without a bot
