Skip to main content

Result handlers

Every manager calls a result handler after a command, component, or event handler finishes — whether it returned normally or threw. A single callback covers both cases, distinguished by the status field.

Run return types

Arcscord normalizes whatever your run() function returns into a Result before passing it to the result handler. You can return any of:

Return valueNormalized to
void / nothingok(true)
stringok(string)
trueok(true)
ok(...) / error(...)passed through unchanged

This means you can keep your handler simple without sacrificing observability:

export const ping = createCommand({
build: { slash: { name: "ping", description: "Ping!" } },
run: async (ctx) => {
await ctx.reply({ content: "Pong!" });
// return nothing — normalized to ok(true)
},
});

The status discriminant

Every result handler receives an object with a status field:

  • "returned"run() returned normally. result holds the normalized Result — it may be ok or error depending on what the handler returned.
  • "thrown"run() threw an unhandled exception. Only thrownValue is present; there is no result field. The thrown value is not pre-wrapped — you decide how to handle it.
managers: {
command: {
resultHandler: (infos) => {
if (infos.status === "thrown") {
// infos.thrownValue is the raw thrown value — could be anything
const err = anyToError(infos.thrownValue);
client.logger.error(`Command threw: ${err.message}`);
return;
}

// infos.status === "returned" — infos.result is available here
const [err, value] = infos.result;
if (err) {
err.generateId();
client.logger.logError(err);
return;
}

client.logger.debug(`Command "${infos.command.build.slash?.name}" returned ${String(value)}`);
},
},
},

Event result handler

Event handlers may return void, string, true, or a full EventHandleResult.

export const messageEvent = createEvent({
event: "messageCreate",
run: (ctx, message) => {
if (message.author.bot) {
return "ignored bot message";
}
// void is fine too
},
});

Configure the result handler via managers.event.resultHandler:

managers: {
event: {
resultHandler: (infos) => {
if (infos.status === "thrown") {
client.logger.error(`Event ${infos.event.name} threw`, {
thrownValue: infos.thrownValue,
});
return;
}

const [err, value] = infos.result;
if (err) {
err.generateId();
client.logger.logError(err);
return;
}

client.logger.debug(`${infos.event.name}${String(value)}`);
},
},
},

The event result handler receives:

Field"returned""thrown"
status"returned""thrown"
resultnormalized EventHandleResult— (not present)
thrownValue— (not present)the raw thrown value
eventthe loaded event handlersame
eventNamethe Discord.js event namesame

Command result handler

Configure via managers.command.resultHandler:

import { MessageFlags } from "discord.js";
import { anyToError } from "@arcscord/error";

managers: {
command: {
resultHandler: async (infos) => {
if (infos.status === "thrown") {
const err = anyToError(infos.thrownValue);
client.logger.error(`Command threw: ${err.message}`);
await infos.interaction.reply({
content: "An unexpected error occurred.",
flags: MessageFlags.Ephemeral,
});
return;
}

const [err, value] = infos.result;
if (err) {
err.generateId();
client.logger.logError(err);
const message = client.getErrorMessage(err.id, infos.locale);
if (infos.defer) {
await infos.interaction.editReply(message);
} else {
await infos.interaction.reply({ ...message, flags: MessageFlags.Ephemeral });
}
return;
}

client.logger.debug(`Command ${infos.command.build.slash?.name ?? "context"} succeeded`);
},
},
},

The command result handler receives:

Field"returned""thrown"
status"returned""thrown"
resultnormalized CommandRunResult— (not present)
thrownValue— (not present)the raw thrown value
interactionDiscord.js command interactionsame
commandthe loaded command handlersame
contextthe Arcscord command contextsame
localedetected i18next language keysame
deferwhether the reply was deferredsame
start / endexecution timestamps (ms)same

Component result handler

Configure via managers.component.resultHandler:

import { MessageFlags } from "discord.js";
import { anyToError } from "@arcscord/error";

managers: {
component: {
resultHandler: async (infos) => {
if (infos.status === "thrown") {
const err = anyToError(infos.thrownValue);
client.logger.error(`Component ${infos.component.route} threw: ${err.message}`);
await infos.interaction.reply({
content: "An unexpected error occurred.",
flags: MessageFlags.Ephemeral,
});
return;
}

const [err] = infos.result;
if (err) {
err.generateId();
client.logger.logError(err);
const message = client.getErrorMessage(err.id, infos.locale);
if (infos.defer) {
await infos.interaction.editReply(message);
} else {
await infos.interaction.reply({ ...message, flags: MessageFlags.Ephemeral });
}
return;
}

client.logger.debug(`Component ${infos.component.route} succeeded`);
},
},
},

The component result handler receives the same fields as the command result handler, with component in place of command.

Default behavior

The default result handlers for all three managers:

  • status === "thrown" — wrap thrownValue in a framework error (CommandError, ComponentError, or EventError), generate an error ID, log it with logError, and send an ephemeral error reply to the user (command and component only).
  • status === "returned", error result — generate an error ID, log it, and send an ephemeral error reply to the user (command and component only).
  • status === "returned", ok result — log at debug level.

The error reply message comes from client.getErrorMessage(id, locale). You can customize it via arcOptions.getErrorMessage on the client.

For more on pre-run failures (command not found, option parsing errors, etc.) and how to configure their logging level and user reply, see Error handling.