Client
ArcClient extends discord.js Client. All discord.js options, properties, events, and REST methods are available on it.
Porting an existing discord.js bot? See Migrate from discord.js for side-by-side examples.
Constructor
import { ArcClient } from "arcscord";
const client = new ArcClient(token, options);
| Parameter | Type | Description |
|---|---|---|
token | string | Bot token from the Discord Developer Portal. |
options | ArcClientOptions | Client configuration. Extends discord.js ClientOptions. |
Options
intents (required)
Gateway intents to enable. Inherited from discord.js. At minimum "Guilds" is needed for slash commands.
const client = new ArcClient(process.env.DISCORD_TOKEN!, {
intents: ["Guilds"],
});
All discord.js ClientOptions (partials, rest, presence, etc.) are also accepted.
applicationId
Discord application ID. When provided, loadCommands (and loadHandlers) can register commands via the REST API before the clientReady event fires, without waiting for discord.js to hydrate client.application.
const client = new ArcClient(process.env.DISCORD_TOKEN!, {
intents: ["Guilds"],
applicationId: process.env.APPLICATION_ID!,
});
logger
Controls the built-in logger. All fields are optional.
| Option | Type | Default | Description |
|---|---|---|---|
level | "trace" | "debug" | "info" | "warn" | "error" | "fatal" | "info" | Minimum level to log. Use "debug" to see command/component execution logs during development. |
format | "pretty" | "json" | "pretty" | Output format. Use "json" in production or containers. |
loggerFunc | (...data: unknown[]) => void | per-level console.log/console.error | Custom function to receive each log line. When omitted, warn/error/fatal go to console.error, everything else to console.log. |
sink | (record: StructuredLogRecord) => void | — | Primary structured output. When set without loggerFunc, replaces the default console output. |
customLogger | LoggerConstructor | ArcLogger | Replace the built-in logger class entirely. Must satisfy LoggerInterface. |
diagnostics | { loggerFunc, format? } | — | Secondary output for full error diagnostics. Providing loggerFunc is what turns it on. |
errorDetail | "short" | "full" | "short" if diagnostics is set, "full" otherwise | How much detail logError/fatalError print on the main sink. |
const client = new ArcClient(process.env.DISCORD_TOKEN!, {
intents: ["Guilds"],
logger: {
level: process.env.NODE_ENV === "production" ? "info" : "debug",
format: process.env.NODE_ENV === "production" ? "json" : "pretty",
},
});
Defaults can also be set via environment variables: ARCSCORD_LOG_LEVEL, LOG_LEVEL, ARCSCORD_LOG_FORMAT, LOG_FORMAT.
enableInternalTrace
Enables verbose trace logs from framework internals — command dispatch, middleware steps, locale detection, etc. Useful for debugging framework behavior.
const client = new ArcClient(process.env.DISCORD_TOKEN!, {
intents: ["Guilds"],
enableInternalTrace: true,
});
Default: false.
waitReady
Defines the default readiness timeout and check interval for the client. This configuration is also used by internal framework calls, such as queued events and loadHandlers when command registration must wait for Discord.
const client = new ArcClient(process.env.DISCORD_TOKEN!, {
intents: ["Guilds"],
waitReady: {
timeout: 15_000,
checkInterval: 100,
},
});
| Option | Type | Default | Description |
|---|---|---|---|
timeout | number | 30000 | Maximum wait in milliseconds before rejecting. |
checkInterval | number | 50 | Delay in milliseconds between readiness checks. |
baseMessages
Overrides framework-generated messages sent to users. Currently supports one key:
baseMessages.error — the message sent when an unhandled error occurs during a command or component handler.
const client = new ArcClient(process.env.DISCORD_TOKEN!, {
intents: ["Guilds"],
baseMessages: {
error: (id, context) => ({
content: `An error occurred (id: \`${id}\`). Please contact support.`,
}),
},
});
The context argument contains:
context.locale— the detected provider language for the interaction.context.t— the deprecated fixed i18next function, only when the legacy locale manager is enabled.
managers
Per-manager configuration. All fields are optional.
| Field | Manages | Documentation |
|---|---|---|
managers.command | Slash, user, and message commands — execution handlers, dispatch diagnostics | Execution handlers |
managers.component | Buttons, select menus, modals — execution handlers, dispatch diagnostics | Execution handlers |
managers.event | Discord.js event listeners — intent checks, execution handlers | Execution handlers |
localization | Adapter, locale mapping, detection, and Discord metadata locales | Localization |
managers.locale | Deprecated v1 i18next compatibility layer; removed in v2 | Localization migration |
Manager lifecycle telemetry is available through the opt-in diagnostics channels.
Example with event intent check configuration:
const client = new ArcClient(process.env.DISCORD_TOKEN!, {
intents: ["Guilds"],
managers: {
event: {
intentCheck: {
missing: "warn", // warn when an event has no matching intent
partialCoverage: "off",
},
},
},
});
Manager properties
ArcClient exposes its managers and the provider-independent localization service:
| Property | Description |
|---|---|
client.commandManager | Registers commands with Discord and dispatches interactions. |
client.componentManager | Routes component custom IDs and dispatches interactions. |
client.eventManager | Wraps discord.js event listeners with execution handling. |
client.localization | Detects provider locales and resolves Discord metadata through the configured adapter. |
client.localeManager | Deprecated v1 i18next compatibility wrapper; removed in v2. |
client.handlersState | Latest handler bootstrap state: idle, loading, ready, or failed. |
Methods
waitReady(options?)
Waits for the Discord client to become ready. Per-call options override the defaults configured through ArcClientOptions.waitReady. The promise rejects with ArcClientReadyTimeoutError when the effective timeout is reached.
This is separate from localization.readyTimeout, which limits how long command loading waits for a localization adapter to initialize.
await client.waitReady({
timeout: 15_000,
checkInterval: 100,
});
| Option | Type | Fallback | Description |
|---|---|---|---|
timeout | number | 30000 | Maximum wait in milliseconds before rejecting. |
checkInterval | number | 50 | Delay in milliseconds between readiness checks. |
The previous numeric form remains supported: waitReady(100) sets checkInterval to 100 ms and keeps the globally configured timeout.
client.isOperational() is stricter than Discord readiness alone: it returns
true only when the Discord client is ready and the latest loadHandlers call
completed successfully.
Loading comes in two tiers with deliberately different error handling:
- The per-category loaders (
loadCommands,loadComponents,loadEvents) areasyncand return an ArcscordResult—[error, count]. On failure the first tuple item is anArcscordErrorwhosecodeidentifies the problem. They never throw for expected failures such as a duplicate route or an unmet intent requirement — you inspect the outcome. - The convenience
loadHandlersis a bootstrap helper that fails fast: it throws the firstArcscordErrorinstead of returning it, so a broken startup crashes loudly rather than continuing in a half-wired state. Reach for the per-category loaders when you want to handle the failure yourself.
loadHandlers(handlers, logs?)
Convenience method. It prevalidates commands, components, and events as one
batch, publishes commands to Discord, then registers every handler locally.
Throws the first ArcscordError on failure; returns a HandlersLoadReport
({ commands, components, events } load counts) on success.
// Fail-fast: an unhandled throw crashes the process at startup.
const report = await client.loadHandlers({
commands: [avatarCommand, pingCommand],
components: [simpleButton, profileModal],
events: [messageEvent],
});
// report -> { commands: 2, components: 2, events: 1 }
// Or handle it explicitly:
try {
await client.loadHandlers(handlers);
}
catch (err) {
client.logger.fatalError(err);
process.exit(1);
}
If applicationId is set, commands are registered immediately over REST without waiting for clientReady. Otherwise, loadHandlers waits for the client to be ready before pushing commands.
Any clientReady handlers in the validated batch are registered before that
wait so they still observe the lifecycle event; all other local handlers are
registered after command publication.
Local registration is atomic for each call. If a failure occurs after local registration starts, Arcscord removes only the events, components, and command entries added by that call; handlers loaded by earlier calls remain available. Discord REST operations are external side effects: once Discord has accepted a command mutation, Arcscord cannot guarantee that the remote change can be rolled back if a later local registration step fails.
loadCommands(commands, group?, guild?)
Registers commands with Discord and loads them into the command manager. Returns Result<number, ArcscordError> (the count of loaded commands); inspect error.code to distinguish validation, application, and registration failures.
const [err] = await client.loadCommands([pingCommand, avatarCommand]);
if (err !== null) {
client.logger.fatalError(err);
}
// Guild-scoped registration
await client.loadCommands([adminCommand], "admin", process.env.GUILD_ID!);
The optional group parameter is an internal label for the command set (used by deleteUnloadedCommands). The optional guild parameter restricts registration to a specific guild.
loadComponents(components)
Loads component handlers into the component manager. Returns Result<number, ArcscordError> (the count of loaded components). Fails with COMPONENT_VALIDATION_FAILED, COMPONENT_ROUTE_DUPLICATE, or COMPONENT_ROUTE_INVALID.
const [err, count] = await client.loadComponents([simpleButton, profileModal, roleMenu]);
Use client.componentManager.unloadComponent(route) to remove a previously loaded component; it returns true when a component was removed.
loadEvents(events)
Registers event handlers and attaches discord.js listeners. Returns Result<number, ArcscordError> (the count of loaded events). Fails with EVENT_HANDLER_DUPLICATE or EVENT_INTENT_MISSING.
const [err, count] = await client.loadEvents([messageEvent, inviteEvent]);
Use client.eventManager.unloadEvent(name) to remove a previously loaded event.
createLogger(name)
Returns a new logger instance scoped to the given name, using the same output function and configuration as the client logger.
const log = client.createLogger("my-module");
log.info("started");
Full setup example
import { ArcClient } from "arcscord";
import { avatarCommand, pingCommand } from "./commands";
import { simpleButton, profileModal } from "./components";
import { messageEvent } from "./events";
const client = new ArcClient(process.env.DISCORD_TOKEN!, {
intents: ["Guilds"],
applicationId: process.env.APPLICATION_ID,
logger: {
level: process.env.NODE_ENV === "production" ? "info" : "debug",
format: process.env.NODE_ENV === "production" ? "json" : "pretty",
},
managers: {
event: {
intentCheck: { missing: "warn" },
},
},
});
await client.loadHandlers({
commands: [avatarCommand, pingCommand],
components: [simpleButton, profileModal],
events: [messageEvent],
});
void client.login();