Skip to main content

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);
ParameterTypeDescription
tokenstringBot token from the Discord Developer Portal.
optionsArcClientOptionsClient 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.

OptionTypeDefaultDescription
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[]) => voidper-level console.log/console.errorCustom 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.
customLoggerLoggerConstructorArcLoggerReplace 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" otherwiseHow 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,
},
});
OptionTypeDefaultDescription
timeoutnumber30000Maximum wait in milliseconds before rejecting.
checkIntervalnumber50Delay 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.

FieldManagesDocumentation
managers.commandSlash, user, and message commands — execution handlers, dispatch diagnosticsExecution handlers
managers.componentButtons, select menus, modals — execution handlers, dispatch diagnosticsExecution handlers
managers.eventDiscord.js event listeners — intent checks, execution handlersExecution handlers
localizationAdapter, locale mapping, detection, and Discord metadata localesLocalization
managers.localeDeprecated v1 i18next compatibility layer; removed in v2Localization 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:

PropertyDescription
client.commandManagerRegisters commands with Discord and dispatches interactions.
client.componentManagerRoutes component custom IDs and dispatches interactions.
client.eventManagerWraps discord.js event listeners with execution handling.
client.localizationDetects provider locales and resolves Discord metadata through the configured adapter.
client.localeManagerDeprecated v1 i18next compatibility wrapper; removed in v2.
client.handlersStateLatest 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,
});
OptionTypeFallbackDescription
timeoutnumber30000Maximum wait in milliseconds before rejecting.
checkIntervalnumber50Delay 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) are async and return an Arcscord Result — [error, count]. On failure the first tuple item is an ArcscordError whose code identifies the problem. They never throw for expected failures such as a duplicate route or an unmet intent requirement — you inspect the outcome.
  • The convenience loadHandlers is a bootstrap helper that fails fast: it throws the first ArcscordError instead 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​

src/index.ts
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();