Guardian of order! Consul Caesar.js noticed that standard alarm signals do not always capture the specifics of situations at the Roman fort. It is time to create your own warning system - Custom Exceptions that will precisely describe every unusual situation in our legion!
In the provinces, legionaries encounter situations that the creators of standard error codes did not foresee. A barbarian ambush, a legion mutiny, a cursed tribute - each of these situations requires a special response protocol. Custom Exceptions in NestJS allow us to create our own, precise problem descriptions.
1// exceptions/base/legionaries.exception.ts
2export abstract class LegionaryException extends Error {
3 public readonly code: string;
4 public readonly context: Record<string, any>;
5 public readonly severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
6 public readonly recoverable: boolean;
7 public readonly timestamp: Date;
8
9 constructor(
10 message: string,
11 code: string,
12 severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' = 'MEDIUM',
13 recoverable: boolean = true,
14 context: Record<string, any> = {}
15 ) {
16 super(message);
17 this.name = this.constructor.name;
18 this.code = code;
19 this.severity = severity;
20 this.recoverable = recoverable;
21 this.context = context;
22 this.timestamp = new Date();
23
24 // Preserve stack trace
25 Error.captureStackTrace(this, this.constructor);
26 }
27
28 toJSON() {
29 return {
30 name: this.name,
31 message: this.message,
32 code: this.code,
33 severity: this.severity,
34 recoverable: this.recoverable,
35 context: this.context,
36 timestamp: this.timestamp.toISOString(),
37 stack: this.stack
38 };
39 }
40
41 // Helper methods
42 isCritical(): boolean {
43 return this.severity === 'CRITICAL';
44 }
45
46 isRecoverable(): boolean {
47 return this.recoverable;
48 }
49
50 getRecoveryInstructions(): string[] {
51 return ['Contact the centurion', 'Check the documentation'];
52 }
53}1// exceptions/tribute/tribute.exceptions.ts
2export class TributeNotFoundException extends LegionaryException {
3 constructor(tributeId: string | number, searchLocation?: string) {
4 super(
5 `tributy o ID ${tributeId} was not found${searchLocation ? ` at location: ${searchLocation}` : ''}`,
6 'TRIBUTE_NOT_FOUND',
7 'MEDIUM',
8 true,
9 { tributeId, searchLocation }
10 );
11 }
12
13 getRecoveryInstructions(): string[] {
14 return [
15 'Check if the tribute ID is correct',
16 'Check if the tribute has been moved',
17 'Use the tribute search function',
18 'Check the transaction history'
19 ];
20 }
21}
22
23export class TributeAlreadyClaimedException extends LegionaryException {
24 constructor(tributeId: string | number, claimedBy: string, claimedAt: Date) {
25 super(
26 `tributy ${tributeId} has already been claimed by ${claimedBy} on ${claimedAt.toLocaleDateString()}`,
27 'TRIBUTE_ALREADY_CLAIMED',
28 'HIGH',
29 false,
30 { tributeId, claimedBy, claimedAt }
31 );
32 }
33
34 getRecoveryInstructions(): string[] {
35 return [
36 'Check available tributes at other locations',
37 'Contact the current owner about an exchange',
38 'Wait for the tribute to be released',
39 'Search for similar tributes'
40 ];
41 }
42}
43
44export class CursedTributeException extends LegionaryException {
45 constructor(
46 tributeId: string | number,
47 curseName: string,
48 curseLevel: 'MINOR' | 'MAJOR' | 'LEGENDARY'
49 ) {
50 const severity = curseLevel === 'LEGENDARY' ? 'CRITICAL' :
51 curseLevel === 'MAJOR' ? 'HIGH' : 'MEDIUM';
52
53 super(
54 `tributy ${tributeId} is affected by a curse: ${curseName}. Level: ${curseLevel}`,
55 'CURSED_TRIBUTE',
56 severity,
57 curseLevel !== 'LEGENDARY',
58 { tributeId, curseName, curseLevel }
59 );
60 }
61
62 getRecoveryInstructions(): string[] {
63 const { curseLevel } = this.context;
64
65 if (curseLevel === 'LEGENDARY') {
66 return [
67 'IMMEDIATELY leave the tribute area!',
68 'Contact the centurion',
69 'Call a curse specialist',
70 'Do not touch the tribute without protection'
71 ];
72 }
73
74 if (curseLevel === 'MAJOR') {
75 return [
76 'Use protective amulets',
77 'Work in a group of at least 3 legionaries',
78 'Perform a purification ritual',
79 'Consult the legion shaman'
80 ];
81 }
82
83 return [
84 'Exercise caution when moving',
85 'Monitor your well-being',
86 'Use protective gloves'
87 ];
88 }
89}
90
91export class TributeCapacityExceededException extends LegionaryException {
92 constructor(
93 chestId: string | number,
94 currentCapacity: number,
95 maxCapacity: number,
96 attemptedWeight: number
97 ) {
98 super(
99 `Skrzynia ${chestId} would exceed capacity! Current: ${currentCapacity}/${maxCapacity}kg, attempted to add: ${attemptedWeight}kg`,
100 'TRIBUTE_CAPACITY_EXCEEDED',
101 'MEDIUM',
102 true,
103 { chestId, currentCapacity, maxCapacity, attemptedWeight, availableSpace: maxCapacity - currentCapacity }
104 );
105 }
106
107 getRecoveryInstructions(): string[] {
108 const { availableSpace } = this.context;
109
110 return [
111 `Available space: ${availableSpace}kg`,
112 'Move some tributes to another chest',
113 'Find a chest with larger capacity',
114 'Check if all tributes are necessary',
115 'Consider selling less valuable items'
116 ];
117 }
118}1// exceptions/legion/legion.exceptions.ts
2export class InsufficientLegionException extends LegionaryException {
3 constructor(
4 operation: string,
5 requiredLegion: number,
6 availableLegion: number,
7 requiredRanks?: string[]
8 ) {
9 super(
10 `Operacja "${operation}" requires ${requiredLegion} legionaries, available: ${availableLegion}${requiredRanks ? `. Required ranks: ${requiredRanks.join(', ')}` : ''}`,
11 'INSUFFICIENT_LEGION',
12 'HIGH',
13 true,
14 { operation, requiredLegion, availableLegion, shortage: requiredLegion - availableLegion, requiredRanks }
15 );
16 }
17
18 getRecoveryInstructions(): string[] {
19 const { shortage, requiredRanks } = this.context;
20
21 const instructions = [
22 `Recruit at least ${shortage} additional legionaries`,
23 'Check legionary availability at ports',
24 'Consider temporarily hiring legionaries'
25 ];
26
27 if (requiredRanks?.length > 0) {
28 instructions.push(`Make sure you have legionaries of ranks: ${requiredRanks.join(', ')}`);
29 }
30
31 return instructions;
32 }
33}
34
35export class LegionMutinyException extends LegionaryException {
36 constructor(
37 leader: string,
38 supporters: number,
39 grievances: string[]
40 ) {
41 super(
42 `Legion mutiny! Leader: ${leader}, number of rebels: ${supporters}. Reasons: ${grievances.join(', ')}`,
43 'LEGION_MUTINY',
44 'CRITICAL',
45 true,
46 { leader, supporters, grievances, timestamp: new Date() }
47 );
48 }
49
50 getRecoveryInstructions(): string[] {
51 return [
52 'IMMEDIATELY call a legion assembly',
53 'Listen to the rebels complaints',
54 'Negotiate with the mutiny leader',
55 'Consider concessions on working conditions',
56 'As a last resort, use centurion authority',
57 'Contact the legion legate'
58 ];
59 }
60}
61
62export class LegionaryDeserterException extends LegionaryException {
63 constructor(
64 userName: string,
65 rank: string,
66 reason: string,
67 takenItems?: string[]
68 ) {
69 super(
70 `Legionariusz ${userName} (${rank}) has left the fort. Reason: ${reason}${takenItems?.length ? `. Took: ${takenItems.join(', ')}` : ''}`,
71 'LEGIONARY_DESERTER',
72 'HIGH',
73 true,
74 { userName, rank, reason, takenItems, desertion_time: new Date() }
75 );
76 }
77
78 getRecoveryInstructions(): string[] {
79 const { rank, takenItems } = this.context;
80
81 const instructions = [
82 `Find a replacement for rank: ${rank}`,
83 'Update the legion roster',
84 'Check if they took any important items'
85 ];
86
87 if (takenItems?.length > 0) {
88 instructions.push('Consider issuing a warrant for the stolen items');
89 }
90
91 return instructions;
92 }
93}1// exceptions/cohort/cohort.exceptions.ts
2export class LegionDamageException extends LegionaryException {
3 constructor(
4 cohortName: string,
5 damageType: 'MINOR' | 'MAJOR' | 'CRITICAL',
6 affectedSystems: string[],
7 repairCost: number
8 ) {
9 const severity = damageType === 'CRITICAL' ? 'CRITICAL' :
10 damageType === 'MAJOR' ? 'HIGH' : 'MEDIUM';
11
12 super(
13 `Cohort ${cohortName} suffered damage of type ${damageType}. Affected systems: ${affectedSystems.join(', ')}. Repair cost: ${repairCost} denarii`,
14 'COHORT_DAMAGE',
15 severity,
16 damageType !== 'CRITICAL',
17 { cohortName, damageType, affectedSystems, repairCost, combatReady: damageType !== 'CRITICAL' }
18 );
19 }
20
21 getRecoveryInstructions(): string[] {
22 const { damageType, repairCost, combatReady } = this.context;
23
24 if (!combatReady) {
25 return [
26 'IMMEDIATELY cease operations!',
27 'Evacuate the cohort from endangered areas',
28 'Call for legion assistance',
29 'Prepare an orderly retreat',
30 'Do not attempt to march in this condition!'
31 ];
32 }
33
34 const instructions = [
35 `Repair cost: ${repairCost} denarii`,
36 'Find the nearest port with a fort',
37 'Order necessary repair materials'
38 ];
39
40 if (damageType === 'MAJOR') {
41 instructions.push('Limit speed to 50% of maximum');
42 }
43
44 return instructions;
45 }
46}
47
48export class LegionOverloadException extends LegionaryException {
49 constructor(
50 cohortName: string,
51 currentWeight: number,
52 maxWeight: number,
53 excessWeight: number
54 ) {
55 super(
56 `Cohort ${cohortName} is overloaded! Weight: ${currentWeight}/${maxWeight}t (excess: ${excessWeight}t)`,
57 'COHORT_OVERLOAD',
58 'HIGH',
59 true,
60 { cohortName, currentWeight, maxWeight, excessWeight, overloadPercentage: (excessWeight / maxWeight) * 100 }
61 );
62 }
63
64 getRecoveryInstructions(): string[] {
65 const { excessWeight, overloadPercentage } = this.context;
66
67 const instructions = [
68 `Remove at least ${excessWeight}t of cargo`,
69 'Check weight distribution on the cohort',
70 'Consider transferring some cargo to another fort'
71 ];
72
73 if (overloadPercentage > 20) {
74 instructions.unshift('URGENT: The cohort may sink!');
75 }
76
77 return instructions;
78 }
79}1// exceptions/weather/weather.exceptions.ts
2export class DangerousWeatherException extends LegionaryException {
3 constructor(
4 weatherType: 'STORM' | 'HURRICANE' | 'FOG' | 'CALM' | 'TSUNAMI',
5 intensity: number, // 1-10
6 duration: number, // w hoursach
7 affectedArea: string
8 ) {
9 const severity = intensity >= 8 ? 'CRITICAL' :
10 intensity >= 6 ? 'HIGH' : 'MEDIUM';
11
12 super(
13 `Dangerous weather: ${weatherType} with intensity ${intensity}/10 in area ${affectedArea}. Duration: ${duration}h`,
14 'DANGEROUS_WEATHER',
15 severity,
16 intensity < 8,
17 { weatherType, intensity, duration, affectedArea, safeToMarch: intensity < 6 }
18 );
19 }
20
21 getRecoveryInstructions(): string[] {
22 const { weatherType, intensity, safeToMarch, duration } = this.context;
23
24 if (!safeToMarch) {
25 return [
26 'IMMEDIATELY seek safe encampment!',
27 'Do not set out to march in these conditions!',
28 'Prepare the fort for possible damage',
29 'Check rescue equipment',
30 `Expected duration: ${duration} godzin`
31 ];
32 }
33
34 const instructions = [
35 'Increase vigilance on the cohort',
36 'Check cargo securing',
37 'Monitor weather forecasts'
38 ];
39
40 switch (weatherType) {
41 case 'FOG':
42 instructions.push('Use sound signals', 'Reduce speed');
43 break;
44 case 'STORM':
45 instructions.push('Secure all loose items', 'Prepare water pumps');
46 break;
47 case 'CALM':
48 instructions.push('Conserve water and food supplies', 'Prepare emergency oars');
49 break;
50 }
51
52 return instructions;
53 }
54}1// exceptions/business/trading.exceptions.ts
2export class InsufficientFundsException extends LegionaryException {
3 constructor(
4 requiredAmount: number,
5 availableAmount: number,
6 currency: string = 'dublony'
7 ) {
8 super(
9 `Insufficient funds! Required: ${requiredAmount} ${currency}, available: ${availableAmount} ${currency}`,
10 'INSUFFICIENT_FUNDS',
11 'MEDIUM',
12 true,
13 { requiredAmount, availableAmount, shortage: requiredAmount - availableAmount, currency }
14 );
15 }
16
17 getRecoveryInstructions(): string[] {
18 const { shortage, currency } = this.context;
19
20 return [
21 `Obtain additional ${shortage} ${currency}`,
22 'Sell unnecessary tributes',
23 'Complete additional missions',
24 'Consider a loan from other centurions',
25 'Negotiate a lower price'
26 ];
27 }
28}
29
30export class InvalidTradingPartnerException extends LegionaryException {
31 constructor(
32 partnerName: string,
33 reason: 'HOSTILE' | 'BANKRUPT' | 'UNRELIABLE' | 'BANNED'
34 ) {
35 const reasonMessages = {
36 'HOSTILE': 'is hostile',
37 'BANKRUPT': 'is bankrupt',
38 'UNRELIABLE': 'is unreliable',
39 'BANNED': 'is on the blacklist'
40 };
41
42 super(
43 `Cannot trade with ${partnerName} - ${reasonMessages[reason]}`,
44 'INVALID_TRADING_PARTNER',
45 reason === 'HOSTILE' ? 'HIGH' : 'MEDIUM',
46 true,
47 { partnerName, reason }
48 );
49 }
50
51 getRecoveryInstructions(): string[] {
52 const { reason } = this.context;
53
54 switch (reason) {
55 case 'HOSTILE':
56 return [
57 'Find a neutral intermediary',
58 'Offer a truce flag',
59 'Check if the relationship can be repaired',
60 'Look for alternative partners'
61 ];
62 case 'BANKRUPT':
63 return [
64 'Check if the partner has regained solvency',
65 'Consider trading on credit',
66 'Find other buyers'
67 ];
68 case 'UNRELIABLE':
69 return [
70 'Demand upfront payment',
71 'Use escrow services',
72 'Negotiate guarantees',
73 'Consider ending the partnership'
74 ];
75 case 'BANNED':
76 return [
77 'Check the reason for the blacklist placement',
78 'File a rehabilitation request',
79 'Find other partners'
80 ];
81 default:
82 return ['Contact the trade centurion'];
83 }
84 }
85}A factory for creating exceptions with context:
1// exceptions/factory/exception.factory.ts
2@Injectable()
3export class LegionaryExceptionFactory {
4 private readonly logger = new Logger(LegionaryExceptionFactory.name);
5
6 createTributeNotFound(tributeId: string | number, context?: any): TributeNotFoundException {
7 const searchLocation = context?.cohort ? `fort ${context.cohort}` : undefined;
8 const exception = new TributeNotFoundException(tributeId, searchLocation);
9
10 this.logException(exception, context);
11 return exception;
12 }
13
14 createInsufficientLegion(
15 operation: string,
16 required: number,
17 available: number,
18 context?: any
19 ): InsufficientLegionException {
20 const exception = new InsufficientLegionException(
21 operation,
22 required,
23 available,
24 context?.requiredRanks
25 );
26
27 this.logException(exception, context);
28 return exception;
29 }
30
31 createLegionDamage(
32 cohortName: string,
33 damageType: 'MINOR' | 'MAJOR' | 'CRITICAL',
34 affectedSystems: string[],
35 repairCost: number,
36 context?: any
37 ): LegionDamageException {
38 const exception = new LegionDamageException(cohortName, damageType, affectedSystems, repairCost);
39
40 this.logException(exception, context);
41
42 // Automatic notifications for critical damage
43 if (damageType === 'CRITICAL') {
44 this.sendCriticalAlert(exception, context);
45 }
46
47 return exception;
48 }
49
50 createBusinessLogicException(
51 message: string,
52 code: string,
53 context?: any
54 ): LegionaryException {
55 class DynamicLegionaryException extends LegionaryException {}
56
57 const exception = new DynamicLegionaryException(message, code, 'MEDIUM', true, context);
58 this.logException(exception, context);
59
60 return exception;
61 }
62
63 private logException(exception: LegionaryException, context?: any): void {
64 this.logger.error(`Exception created: ${exception.code}`, {
65 message: exception.message,
66 code: exception.code,
67 severity: exception.severity,
68 context: exception.context,
69 additionalContext: context
70 });
71 }
72
73 private sendCriticalAlert(exception: LegionDamageException, context?: any): void {
74 // Alert system implementation for critical situations
75 this.logger.fatal('CRITICAL COHORT DAMAGE DETECTED', {
76 exception: exception.toJSON(),
77 context,
78 requiresImmediateAttention: true
79 });
80 }
81}1// guards/tribute.guard.ts
2@Injectable()
3export class TributeGuard implements CanActivate {
4 constructor(private exceptionFactory: LegionaryExceptionFactory) {}
5
6 async canActivate(context: ExecutionContext): Promise<boolean> {
7 const request = context.switchToHttp().getRequest();
8 const tributeId = request.params.id;
9 const legionaries = request.user;
10
11 // Check if the tribute exists
12 const tribute = await this.tributeService.findById(tributeId);
13 if (!tribute) {
14 throw this.exceptionFactory.createTributeNotFound(tributeId, {
15 legionaries: legionaries.name,
16 cohort: legionaries.cohort
17 });
18 }
19
20 // Check if the tribute is cursed
21 if (tribute.isCursed && !legionaries.hasCurseResistance) {
22 throw new CursedTributeException(
23 tributeId,
24 tribute.curseName,
25 tribute.curseLevel
26 );
27 }
28
29 return true;
30 }
31}
32
33// services/legion.service.ts
34@Injectable()
35export class LegionService {
36 constructor(private exceptionFactory: LegionaryExceptionFactory) {}
37
38 async assignToMission(missionId: string, legionariesIds: string[]): Promise<Mission> {
39 const mission = await this.findMission(missionId);
40 const availableLegionarys = await this.getAvailableLegionarys(legionariesIds);
41
42 // Check if we have sufficient cohort
43 if (availableLegionarys.length < mission.requiredLegion) {
44 throw this.exceptionFactory.createInsufficientLegion(
45 mission.name,
46 mission.requiredLegion,
47 availableLegionarys.length,
48 {
49 missionId,
50 requiredRanks: mission.requiredRanks,
51 availableLegionarys: availableLegionarys.map(p => p.name)
52 }
53 );
54 }
55
56 // Check if the cohort has the required skills
57 const missingSkills = this.checkRequiredSkills(mission, availableLegionarys);
58 if (missingSkills.length > 0) {
59 throw this.exceptionFactory.createBusinessLogicException(
60 `The cohort does not have the required skills: ${missingSkills.join(', ')}`,
61 'MISSING_LEGION_SKILLS',
62 {
63 missionId,
64 missingSkills,
65 assignedLegionarys: availableLegionarys.map(p => ({ name: p.name, skills: p.skills }))
66 }
67 );
68 }
69
70 return await this.executeAssignment(mission, availableLegionarys);
71 }
72}Custom Exceptions are your way of precisely communicating unusual situations. Well-designed exceptions not only inform about the problem but also guide the soldiers through the resolution process!
Remember: every good legionary has a plan for every situation, and every good Exception has repair instructions!