From a96c6caa49c8d309726b274fd63bd780f7a22ec1 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Wed, 18 Mar 2026 12:51:16 +0100 Subject: [PATCH] fix: standardize error classes in shared service modules Replace raw `Error` with `UserError` for user-facing conditions (invalid trade state, user not found, permission/channel type checks) and `SystemError` for internal failures (DB insert failures, external API errors, missing config). Improves Discord UX by ensuring user-facing errors are surfaced cleanly via withCommandErrorHandling. Co-Authored-By: Claude Sonnet 4.6 --- shared/modules/economy/economy.service.ts | 2 +- shared/modules/economy/exam.service.ts | 6 ++--- .../game-settings/game-settings.service.ts | 5 ++-- shared/modules/leveling/leveling.service.ts | 3 ++- .../modules/moderation/moderation.service.ts | 3 ++- shared/modules/moderation/prune.service.ts | 9 ++++--- shared/modules/trade/trade.service.ts | 25 ++++++++++--------- shared/modules/trivia/trivia.service.ts | 6 ++--- 8 files changed, 32 insertions(+), 27 deletions(-) diff --git a/shared/modules/economy/economy.service.ts b/shared/modules/economy/economy.service.ts index c7a7a57..f28c71c 100644 --- a/shared/modules/economy/economy.service.ts +++ b/shared/modules/economy/economy.service.ts @@ -96,7 +96,7 @@ export const economyService = { }); if (!user) { - throw new Error("User not found"); + throw new UserError("User not found"); } let streak = (user.dailyStreak || 0) + 1; diff --git a/shared/modules/economy/exam.service.ts b/shared/modules/economy/exam.service.ts index 5f01fe1..1baa6f0 100644 --- a/shared/modules/economy/exam.service.ts +++ b/shared/modules/economy/exam.service.ts @@ -4,7 +4,7 @@ import { TimerType, TransactionType } from "@shared/lib/constants"; import { config } from "@shared/lib/config"; import { withTransaction } from "@/lib/db"; import type { Transaction } from "@shared/lib/types"; -import { UserError } from "@shared/lib/errors"; +import { UserError, SystemError } from "@shared/lib/errors"; const EXAM_TIMER_TYPE = TimerType.EXAM_SYSTEM; const EXAM_TIMER_KEY = 'default'; @@ -86,7 +86,7 @@ export const examService = { // Ensure user exists const { userService } = await import("@shared/modules/user/user.service"); const user = await userService.getOrCreateUser(userId, username, txFn); - if (!user) throw new Error("Failed to get or create user."); + if (!user) throw new SystemError("Failed to get or create user."); const now = new Date(); const currentDay = now.getDay(); @@ -126,7 +126,7 @@ export const examService = { where: eq(users.id, BigInt(userId)) }); - if (!user) throw new Error("User not found"); + if (!user) throw new UserError("User not found"); const timer = await txFn.query.userTimers.findFirst({ where: and( diff --git a/shared/modules/game-settings/game-settings.service.ts b/shared/modules/game-settings/game-settings.service.ts index ba02f40..f02f1f0 100644 --- a/shared/modules/game-settings/game-settings.service.ts +++ b/shared/modules/game-settings/game-settings.service.ts @@ -1,6 +1,7 @@ import { eq } from "drizzle-orm"; import { gameSettings } from "@db/schema"; import { DrizzleClient } from "@shared/db/DrizzleClient"; +import { SystemError } from "@shared/lib/errors"; import type { LevelingConfig, EconomyConfig, @@ -88,7 +89,7 @@ export const gameSettingsService = { const existing = await gameSettingsService.getSettings(false); if (!existing) { - throw new Error("Game settings not found. Initialize settings first."); + throw new SystemError("Game settings not found. Initialize settings first."); } const updates: Partial = { [section]: value }; @@ -101,7 +102,7 @@ export const gameSettingsService = { const settings = await gameSettingsService.getSettings(false); if (!settings) { - throw new Error("Game settings not found. Initialize settings first."); + throw new SystemError("Game settings not found. Initialize settings first."); } const commands = { diff --git a/shared/modules/leveling/leveling.service.ts b/shared/modules/leveling/leveling.service.ts index 7b6f7ed..743c516 100644 --- a/shared/modules/leveling/leveling.service.ts +++ b/shared/modules/leveling/leveling.service.ts @@ -4,6 +4,7 @@ import { withTransaction } from "@/lib/db"; import { config } from "@shared/lib/config"; import type { Transaction } from "@shared/lib/types"; import { TimerKey, TimerType } from "@shared/lib/constants"; +import { UserError } from "@shared/lib/errors"; export const levelingService = { // Calculate total XP required to REACH a specific level (Cumulative) @@ -49,7 +50,7 @@ export const levelingService = { where: eq(users.id, BigInt(id)), }); - if (!user) throw new Error("User not found"); + if (!user) throw new UserError("User not found"); const currentXp = user.xp ?? 0n; const newXp = currentXp + amount; diff --git a/shared/modules/moderation/moderation.service.ts b/shared/modules/moderation/moderation.service.ts index 14ab89c..6200739 100644 --- a/shared/modules/moderation/moderation.service.ts +++ b/shared/modules/moderation/moderation.service.ts @@ -4,6 +4,7 @@ import { DrizzleClient } from "@shared/db/DrizzleClient"; import type { CreateCaseOptions, ClearCaseOptions, SearchCasesFilter } from "@/modules/moderation/moderation.types"; import { getUserWarningEmbed } from "@/modules/moderation/moderation.view"; import { CaseType } from "@shared/lib/constants"; +import { SystemError } from "@shared/lib/errors"; export interface ModerationCaseConfig { dmOnWarn?: boolean; @@ -100,7 +101,7 @@ export const moderationService = { }); if (!moderationCase) { - throw new Error("Failed to create moderation case"); + throw new SystemError("Failed to create moderation case"); } const warningCount = await getActiveWarningCount(options.userId); diff --git a/shared/modules/moderation/prune.service.ts b/shared/modules/moderation/prune.service.ts index 62a10ac..140ad6d 100644 --- a/shared/modules/moderation/prune.service.ts +++ b/shared/modules/moderation/prune.service.ts @@ -2,6 +2,7 @@ import { Collection, Message, PermissionFlagsBits } from "discord.js"; import type { TextBasedChannel } from "discord.js"; import type { PruneOptions, PruneResult, PruneProgress } from "@/modules/moderation/prune.types"; import { config } from "@shared/lib/config"; +import { UserError, SystemError } from "@shared/lib/errors"; /** * Fetch messages from a channel @@ -30,7 +31,7 @@ async function processBatch( userId?: string ): Promise<{ deleted: number; skipped: number }> { if (!('bulkDelete' in channel)) { - throw new Error("This channel type does not support bulk deletion"); + throw new UserError("This channel type does not support bulk deletion"); } // Filter by user if specified @@ -54,7 +55,7 @@ async function processBatch( }; } catch (error) { console.error("Error during bulk delete:", error); - throw new Error("Failed to delete messages"); + throw new SystemError("Failed to delete messages"); } } @@ -76,12 +77,12 @@ export const pruneService = { ): Promise { // Validate channel permissions if (!('permissionsFor' in channel)) { - throw new Error("Cannot check permissions for this channel type"); + throw new UserError("Cannot check permissions for this channel type"); } const permissions = channel.permissionsFor(channel.client.user!); if (!permissions?.has(PermissionFlagsBits.ManageMessages)) { - throw new Error("Missing permission to manage messages in this channel"); + throw new UserError("Missing permission to manage messages in this channel"); } const { amount, userId, all } = options; diff --git a/shared/modules/trade/trade.service.ts b/shared/modules/trade/trade.service.ts index 86f63f6..95f0969 100644 --- a/shared/modules/trade/trade.service.ts +++ b/shared/modules/trade/trade.service.ts @@ -5,6 +5,7 @@ import { itemTransactions } from "@db/schema"; import { withTransaction } from "@/lib/db"; import type { Transaction } from "@shared/lib/types"; import { TransactionType, ItemTransactionType } from "@shared/lib/constants"; +import { UserError } from "@shared/lib/errors"; // Module-level session storage const sessions = new Map(); @@ -114,11 +115,11 @@ export const tradeService = { */ updateMoney: (threadId: string, userId: string, amount: bigint) => { const session = tradeService.getSession(threadId); - if (!session) throw new Error("Session not found"); - if (session.state !== 'NEGOTIATING') throw new Error("Trade is not active"); + if (!session) throw new UserError("Session not found"); + if (session.state !== 'NEGOTIATING') throw new UserError("Trade is not active"); const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null; - if (!participant) throw new Error("User not in trade"); + if (!participant) throw new UserError("User not in trade"); participant.offer.money = amount; unlockAll(session); @@ -127,11 +128,11 @@ export const tradeService = { addItem: (threadId: string, userId: string, item: { id: number, name: string }, quantity: bigint) => { const session = tradeService.getSession(threadId); - if (!session) throw new Error("Session not found"); - if (session.state !== 'NEGOTIATING') throw new Error("Trade is not active"); + if (!session) throw new UserError("Session not found"); + if (session.state !== 'NEGOTIATING') throw new UserError("Trade is not active"); const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null; - if (!participant) throw new Error("User not in trade"); + if (!participant) throw new UserError("User not in trade"); const existing = participant.offer.items.find(i => i.id === item.id); if (existing) { @@ -146,10 +147,10 @@ export const tradeService = { removeItem: (threadId: string, userId: string, itemId: number) => { const session = tradeService.getSession(threadId); - if (!session) throw new Error("Session not found"); + if (!session) throw new UserError("Session not found"); const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null; - if (!participant) throw new Error("User not in trade"); + if (!participant) throw new UserError("User not in trade"); participant.offer.items = participant.offer.items.filter(i => i.id !== itemId); @@ -159,10 +160,10 @@ export const tradeService = { toggleLock: (threadId: string, userId: string): boolean => { const session = tradeService.getSession(threadId); - if (!session) throw new Error("Session not found"); + if (!session) throw new UserError("Session not found"); const participant = session.userA.id === userId ? session.userA : session.userB.id === userId ? session.userB : null; - if (!participant) throw new Error("User not in trade"); + if (!participant) throw new UserError("User not in trade"); participant.locked = !participant.locked; session.lastInteraction = Date.now(); @@ -179,10 +180,10 @@ export const tradeService = { */ executeTrade: async (threadId: string): Promise => { const session = tradeService.getSession(threadId); - if (!session) throw new Error("Session not found"); + if (!session) throw new UserError("Session not found"); if (!session.userA.locked || !session.userB.locked) { - throw new Error("Both players must accept the trade first."); + throw new UserError("Both players must accept the trade first."); } session.state = 'COMPLETED'; // Prevent double execution diff --git a/shared/modules/trivia/trivia.service.ts b/shared/modules/trivia/trivia.service.ts index 6634ff9..998edab 100644 --- a/shared/modules/trivia/trivia.service.ts +++ b/shared/modules/trivia/trivia.service.ts @@ -3,7 +3,7 @@ import { eq, and, sql } from "drizzle-orm"; import { config } from "@shared/lib/config"; import { withTransaction } from "@/lib/db"; import type { Transaction } from "@shared/lib/types"; -import { UserError } from "@shared/lib/errors"; +import { UserError, SystemError } from "@shared/lib/errors"; import { TimerType, TransactionType } from "@shared/lib/constants"; import { DrizzleClient } from "@shared/db/DrizzleClient"; @@ -94,13 +94,13 @@ class TriviaService { const data = await response.json() as OpenTDBResponse; if (data.response_code !== 0 || !data.results || data.results.length === 0) { - throw new Error('Failed to fetch trivia question'); + throw new SystemError('Failed to fetch trivia question'); } const result = data.results[0]; if (!result) { - throw new Error('No trivia question returned'); + throw new SystemError('No trivia question returned'); } // Decode base64