Middleware
Arcscord middlewares run before a command or component handler. They are useful for checks and shared behavior such as permissions, feature flags, telemetry, cooldowns, author-only components, or any other logic that should happen before the main handler.
Middleware support is provided by the core arcscord package through CommandMiddleware and ComponentMiddleware. Ready-to-use middleware implementations are documented separately in the @arcscord/middleware package page.
Result Types
Every middleware returns one of three results:
| Result | Helper | Meaning |
|---|---|---|
| Continue | this.next(value) | Continue to the next middleware or handler and expose value in ctx.additional. |
| Cancel | this.cancel(result) | Stop the middleware chain and do not run the handler. Use this when the middleware already replied or handled the interaction. |
| Error | this.error(error) | Stop the middleware chain and forward the error to Arcscord's error handler. |
Only one result is active at a time. Returned objects always contain next, cancel, and error, with inactive fields set to null.
Execution Flow
Middlewares run in the order they are listed in use.
use: [
new FirstMiddleware(),
new SecondMiddleware(),
];
The flow is:
- Arcscord creates the command or component context.
- Each middleware runs in order.
next(value)stores the value underctx.additional[middleware.name].cancel(result)stops execution after the cancel result resolves successfully.error(error)stops execution and calls the configured error handler.- If every middleware continues, Arcscord runs the command or component handler.
If a middleware throws, Arcscord converts it to a command or component error and forwards it through the same error path.
Command Middleware
Create command middleware by extending CommandMiddleware.
import type { CommandContext, CommandMiddlewareRun } from "arcscord";
import { CommandMiddleware } from "arcscord";
import { MessageFlags } from "discord.js";
const allowedUserIds = new Set(["123456789"]);
type AllowedUserState = {
allowed: true;
};
class AllowedUsersMiddleware extends CommandMiddleware {
readonly name = "allowedUsers" as const;
run(ctx: CommandContext): CommandMiddlewareRun<AllowedUserState> {
if (!allowedUserIds.has(ctx.user.id)) {
return this.cancel(ctx.reply({
content: "You cannot use this command.",
flags: MessageFlags.Ephemeral,
}));
}
return this.next({ allowed: true });
}
}
Use it on a command:
import { createCommand } from "arcscord";
export const adminCommand = createCommand({
build: {
slash: {
name: "admin",
description: "Admin-only command",
},
},
use: [new AllowedUsersMiddleware()],
run: (ctx) => {
const allowedState = ctx.additional.allowedUsers;
return ctx.reply(`Allowed: ${allowedState.allowed}`);
},
});
Command Errors
Use this.error(...) when the middleware cannot handle the failure by replying.
import type { CommandContext, CommandMiddlewareRun } from "arcscord";
import { CommandError, CommandMiddleware } from "arcscord";
class RequiredConfigMiddleware extends CommandMiddleware {
readonly name = "requiredConfig" as const;
run(ctx: CommandContext): CommandMiddlewareRun<{ configured: true }> {
if (!process.env.REQUIRED_CHANNEL_ID) {
return this.error(new CommandError({
ctx,
message: "Missing REQUIRED_CHANNEL_ID",
}));
}
return this.next({ configured: true });
}
}
Component Middleware
Create component middleware by extending ComponentMiddleware.
import type { ComponentContext, ComponentMiddlewareRun } from "arcscord";
import { ComponentMiddleware } from "arcscord";
import { MessageFlags } from "discord.js";
type AuthorState = {
status: "author" | "ignored";
};
class SameAuthorMiddleware extends ComponentMiddleware {
readonly name = "sameAuthor" as const;
run(ctx: ComponentContext): ComponentMiddlewareRun<AuthorState> {
if (!ctx.isMessageComponentContext()) {
return this.next({ status: "ignored" });
}
if (!ctx.message.interactionMetadata) {
return this.next({ status: "ignored" });
}
if (ctx.message.interactionMetadata.user.id !== ctx.user.id) {
return this.cancel(ctx.reply({
content: "Only the original author can use this component.",
flags: MessageFlags.Ephemeral,
}));
}
return this.next({ status: "author" });
}
}
Use it on a component:
import { button, createButton } from "arcscord";
export const secureButton = createButton({
route: "secure_button",
build: id => button({
customId: id(),
label: "Confirm",
style: "success",
}),
use: [new SameAuthorMiddleware()],
run: (ctx) => {
const authorState = ctx.additional.sameAuthor;
return ctx.reply(`Status: ${authorState.status}`);
},
});
Component Errors
Use this.error(...) with ComponentError when the middleware should fail through Arcscord's error handler.
import type { ComponentContext, ComponentMiddlewareRun } from "arcscord";
import { ComponentError, ComponentMiddleware } from "arcscord";
class RequiredRouteParamMiddleware extends ComponentMiddleware {
readonly name = "requiredRouteParam" as const;
run(ctx: ComponentContext): ComponentMiddlewareRun<{ itemId: string }> {
const itemId = ctx.params.itemId;
if (!itemId) {
return this.error(new ComponentError({
interaction: ctx.interaction,
message: "Missing itemId route parameter",
}));
}
return this.next({ itemId });
}
}
Accessing Middleware Values
Values returned with next(value) are stored in ctx.additional using the middleware name.
class UserScopeMiddleware extends CommandMiddleware {
readonly name = "userScope" as const;
run(ctx: CommandContext): CommandMiddlewareRun<{ userId: string }> {
return this.next({ userId: ctx.user.id });
}
}
export const scopedCommand = createCommand({
build: {
slash: {
name: "scope",
description: "Read middleware data",
},
},
use: [new UserScopeMiddleware()],
run: (ctx) => {
return ctx.reply(`User id: ${ctx.additional.userScope.userId}`);
},
});
For best type inference, define middleware names with as const.
readonly name = "userScope" as const;
Middleware names must be unique inside a single use array. Arcscord rejects duplicate names before running any middleware, because duplicate names would overwrite the same ctx.additional[name] entry.
Choosing next, cancel, or error
Use next when the handler should continue:
return this.next({ checked: true });
Use cancel when the middleware handled the interaction and the handler should not run:
return this.cancel(ctx.reply({
content: "You cannot use this.",
flags: MessageFlags.Ephemeral,
}));
Use error when the framework error handler should receive the failure:
return this.error(new ComponentError({
interaction: ctx.interaction,
message: "Unexpected middleware state",
}));
Reusable Localized Middleware Messages
Middlewares from @arcscord/middleware accept message factories. These factories receive middleware data plus ctx, locale, and t, so you can define the user-facing message once and reuse it across commands.
import type { CooldownMessageOptions, MessageOptions } from "@arcscord/middleware";
import type { CommandContext } from "arcscord";
import { CooldownMiddleware } from "@arcscord/middleware";
const cooldownMessage: MessageOptions<CooldownMessageOptions, CommandContext> = ({ cooldownRemaining, t }) => ({
content: t($ => $.middleware.cooldown, {
seconds: Math.ceil(cooldownRemaining / 1000),
}),
});
export const createCooldownMiddleware = (duration: number) => {
return new CooldownMiddleware(duration, cooldownMessage);
};
Then each command only chooses the middleware configuration:
use: [
createCooldownMiddleware(10),
];
Best Practices
- Keep middleware focused on one responsibility.
- Use stable
namevalues withas constsoctx.additionalis typed correctly. - Prefer
cancelfor expected user-facing blocks such as missing permissions or cooldowns. - Prefer
errorfor invalid configuration, unexpected state, or failures that should be logged centrally. - Return small serializable objects from
next; they are easier to inspect and test. - Put reusable middleware in shared files or packages instead of duplicating checks in handlers.