LogoRobo.js

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

The recommended way to hook into startup and shutdown is with Robo lifecycle files. These run for all Robo projects, not just Discord bots.

start.tsStartup lifecycle
stop.tsShutdown lifecycle

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.

src/robo/start.ts
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()
}
src/robo/start.js
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.

src/robo/stop.ts
import type { StopContext } from 'robo.js'

export default async (context: StopContext) => {
	context.logger.info(`Shutting down (reason: ${context.reason})...`)
	await saveState()
	await disconnectDatabase()
}
src/robo/stop.js
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.

src/events/clientReady.ts
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`)
}
src/events/clientReady.js
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.tsclientReady event
WhenDuring Robo startupAfter Discord gateway connection
Client stateClient exists but may not be logged inClient is fully connected, caches populated
Use forGeneral startup logic, data loadingDiscord-specific setup that needs cached data
FrameworkRobo.js (works for all project types)Discord.js gateway event

Handler loading

The plugin uses two different loading strategies depending on the environment:

ModeStrategyReason
ProductionEager loadingAll handlers are imported in parallel during prepare for fastest runtime access
DevelopmentLazy loadingHandlers are imported on-demand for faster startup and HMR support
MockLazy loadingAvoids 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:

  1. Server wait: The start hook waits for @robojs/server to be ready before logging in, since the mock server must be listening before Discord.js can connect to its mock gateway
  2. Runtime registration: Commands are registered at runtime instead of build time, since the build/complete step is skipped in mock mode
  3. Standalone mode: When __ROBO_MOCK_STANDALONE=true, the start hook skips login entirely -- the mock server runs without a bot

Next steps

On this page