We use cookies to enhance your experience on the site
CodeWorlds

Custom Error Classes

Welcome back to Jurassic Park! In previous exercises we learned about the different kinds of errors in JavaScript and the basics of handling them with

try...catch...finally
. Now we'll dive deeper into creating custom, non-standard error classes, which are invaluable in large applications — like systems for managing a park full of prehistoric reptiles.

Why Custom Error Classes?

In complex systems like Jurassic Park, we need precise problem categorization. It's not enough to know "something went wrong" — we need to know exactly what happened, where, and how to react.

Custom error classes allow:

  1. Precisely categorizing different types of problems
  2. Attaching additional contextual data to errors
  3. Implementing custom helper methods
  4. Easier filtering and handling of specific cases

It's like the difference between a general park alarm and a precise message: "Velociraptors escaped from enclosure B6, last seen heading toward the visitor center."

Creating a Custom Error Hierarchy

For Jurassic Park we'll create a complete error hierarchy that reflects the domain:

1// Base error class for all park problems
2class JurassicParkError extends Error {
3  constructor(message, code, severity = "medium") {
4    super(message);
5    this.name = "JurassicParkError";
6    this.code = code;
7    this.severity = severity;
8    this.timestamp = new Date().toISOString();
9
10    // Preserve the stack trace
11    if (Error.captureStackTrace) {
12      Error.captureStackTrace(this, this.constructor);
13    }
14  }
15
16  toJSON() {
17    return {
18      name: this.name,
19      message: this.message,
20      code: this.code,
21      severity: this.severity,
22      timestamp: this.timestamp,
23      stack: this.stack
24    };
25  }
26
27  toString() {
28    return `[${this.severity.toUpperCase()}] ${this.name} (${this.code}): ${this.message}`;
29  }
30}
31
32// Dinosaur-related errors
33class DinosaurError extends JurassicParkError {
34  constructor(message, code, dinosaurId, species, severity = "high") {
35    super(message, code, severity);
36    this.name = "DinosaurError";
37    this.dinosaurId = dinosaurId;
38    this.species = species;
39  }
40}
41
42class DinosaurHealthError extends DinosaurError {
43  constructor(dinosaurId, species, healthIssue, vitalSigns) {
44    super(
45      `Health issue with ${species} (ID: ${dinosaurId}): ${healthIssue}`,
46      "DINO_HEALTH_ISSUE",
47      dinosaurId,
48      species,
49      "medium"
50    );
51    this.name = "DinosaurHealthError";
52    this.healthIssue = healthIssue;
53    this.vitalSigns = vitalSigns;
54  }
55}
56
57class DinosaurEscapeError extends DinosaurError {
58  constructor(dinosaurId, species, lastKnownLocation) {
59    super(
60      `${species} (ID: ${dinosaurId}) has escaped! Last known location: ${lastKnownLocation}`,
61      "DINO_ESCAPE",
62      dinosaurId,
63      species,
64      "critical"
65    );
66    this.name = "DinosaurEscapeError";
67    this.lastKnownLocation = lastKnownLocation;
68  }
69}
70
71// Infrastructure errors
72class ParkInfrastructureError extends JurassicParkError {
73  constructor(message, code, system, severity = "high") {
74    super(message, code, severity);
75    this.name = "ParkInfrastructureError";
76    this.system = system;
77  }
78}
79
80class PowerSystemError extends ParkInfrastructureError {
81  constructor(sector, powerLevel, backupAvailable) {
82    super(
83      `Power failure in sector ${sector}. Level: ${powerLevel}%. Backup: ${backupAvailable ? 'available' : 'unavailable'}`,
84      "POWER_FAILURE",
85      "power",
86      backupAvailable ? "medium" : "critical"
87    );
88    this.name = "PowerSystemError";
89    this.sector = sector;
90    this.powerLevel = powerLevel;
91    this.backupAvailable = backupAvailable;
92  }
93}
94
95class FenceSystemError extends ParkInfrastructureError {
96  constructor(sector, fenceId, integrity) {
97    super(
98      `Fence failure in sector ${sector}: ${fenceId}, integrity: ${integrity}%`,
99      "FENCE_FAILURE",
100      "fence",
101      integrity < 50 ? "critical" : "high"
102    );
103    this.name = "FenceSystemError";
104    this.sector = sector;
105    this.fenceId = fenceId;
106    this.integrity = integrity;
107  }
108}

Using the Error Hierarchy

1// Incident management system
2class IncidentManagementSystem {
3  constructor() {
4    this.incidents = [];
5    this.alertLevel = "green";
6  }
7
8  reportIncident(error) {
9    const incident = {
10      id: `INC-${Date.now()}`,
11      error: error.toJSON(),
12      status: "open",
13      reportedAt: new Date().toISOString(),
14      actions: []
15    };
16
17    this.incidents.push(incident);
18    this._updateAlertLevel(error);
19    this._dispatchResponse(error, incident.id);
20
21    return incident.id;
22  }
23
24  _updateAlertLevel(error) {
25    const severityOrder = { low: 0, medium: 1, high: 2, critical: 3 };
26    const currentLevel = this.alertLevel;
27
28    if (error.severity === "critical") {
29      this.alertLevel = "red";
30    } else if (error.severity === "high" && currentLevel !== "red") {
31      this.alertLevel = "orange";
32    } else if (error.severity === "medium" && !["red", "orange"].includes(currentLevel)) {
33      this.alertLevel = "yellow";
34    }
35
36    if (this.alertLevel !== currentLevel) {
37      console.log(`Alert level changed: ${currentLevel}${this.alertLevel}`);
38    }
39  }
40
41  _dispatchResponse(error, incidentId) {
42    if (error instanceof DinosaurEscapeError) {
43      console.log(`CRITICAL: Activating emergency protocol for ${error.species} escape!`);
44      console.log(`Last known location: ${error.lastKnownLocation}`);
45      // Activate lockdown, notify security teams, etc.
46    } else if (error instanceof PowerSystemError) {
47      console.log(`Power failure in sector ${error.sector}`);
48      if (!error.backupAvailable) {
49        console.log("CRITICAL: No backup power! Fences may be inactive!");
50      }
51    } else if (error instanceof FenceSystemError) {
52      console.log(`Fence failure: sector ${error.sector}, integrity ${error.integrity}%`);
53      if (error.integrity < 50) {
54        console.log("CRITICAL: Fence integrity critical — potential breach risk!");
55      }
56    } else if (error instanceof DinosaurHealthError) {
57      console.log(`Health alert for ${error.species}: ${error.healthIssue}`);
58    } else {
59      console.log(`General incident reported: ${error.message}`);
60    }
61  }
62
63  getIncidentById(id) {
64    return this.incidents.find(i => i.id === id);
65  }
66
67  resolveIncident(id, resolution) {
68    const incident = this.getIncidentById(id);
69    if (incident) {
70      incident.status = "resolved";
71      incident.resolvedAt = new Date().toISOString();
72      incident.resolution = resolution;
73    }
74    return incident;
75  }
76}
77
78// Example usage
79const incidentSystem = new IncidentManagementSystem();
80
81// Simulate a dinosaur escape
82try {
83  const fenceIntegrity = 20; // 20% — very low!
84  if (fenceIntegrity < 30) {
85    throw new FenceSystemError("B6", "FENCE-B6-NORTH", fenceIntegrity);
86  }
87} catch (error) {
88  const incidentId = incidentSystem.reportIncident(error);
89  console.log(`Incident recorded: ${incidentId}`);
90  console.log(error.toString());
91}
92
93// Simulate a dinosaur health issue
94try {
95  const vitalSigns = { heartRate: 180, temperature: 41.5, respiratory: 45 };
96  if (vitalSigns.temperature > 41) {
97    throw new DinosaurHealthError("VEL-02", "Velociraptor", "High fever", vitalSigns);
98  }
99} catch (error) {
100  const incidentId = incidentSystem.reportIncident(error);
101  console.log(`Health incident: ${incidentId}`);
102}

Error Serialization

One of the problems with error objects in JavaScript is their serialization. By default, when we try to convert an error to JSON, we lose most of the information:

1const error = new Error('Test error');
2console.log(JSON.stringify(error)); // {} - empty object!

Custom error classes can solve this problem by implementing a

toJSON
method:

1class SerializableError extends Error {
2  constructor(message) {
3    super(message);
4    this.name = this.constructor.name;
5  }
6
7  toJSON() {
8    return {
9      name: this.name,
10      message: this.message,
11      stack: this.stack,
12      // Additional properties specific to subclasses
13      ...Object.getOwnPropertyNames(this)
14        .filter(prop => !['name', 'message', 'stack'].includes(prop))
15        .reduce((obj, prop) => {
16          obj[prop] = this[prop];
17          return obj;
18        }, {})
19    };
20  }
21}
22
23// Now we can create serializable errors
24class DinosaurEscapeError extends SerializableError {
25  constructor(message, species, location) {
26    super(message);
27    this.species = species;
28    this.location = location;
29    this.timestamp = new Date();
30  }
31}
32
33const error = new DinosaurEscapeError(
34  'Dinosaur escaped from enclosure',
35  'Velociraptor',
36  'Sector B6'
37);
38
39// Now the error can be properly serialized
40console.log(JSON.stringify(error, null, 2));
41/* Output:
42{
43  "name": "DinosaurEscapeError",
44  "message": "Dinosaur escaped from enclosure",
45  "stack": "DinosaurEscapeError: Dinosaur escaped from enclosure\n    at ...",
46  "species": "Velociraptor",
47  "location": "Sector B6",
48  "timestamp": "2023-05-15T10:30:45.123Z"
49}
50*/

This is especially useful when we need to send error information through an API or save it in logs.

Handling Asynchronous Errors with Custom Classes

Custom error classes work great with asynchronous code:

1// API-specific error class
2class APIError extends Error {
3  constructor(message, statusCode, endpoint) {
4    super(message);
5    this.name = 'APIError';
6    this.statusCode = statusCode;
7    this.endpoint = endpoint;
8    this.timestamp = new Date();
9  }
10
11  isServerError() {
12    return this.statusCode >= 500;
13  }
14
15  isClientError() {
16    return this.statusCode >= 400 && this.statusCode < 500;
17  }
18
19  isAuthError() {
20    return this.statusCode === 401 || this.statusCode === 403;
21  }
22}
23
24// Function fetching data from API that uses a custom error class
25async function fetchDinosaurData(dinoId) {
26  try {
27    const response = await fetch(`https://jurassic-park-api.com/dinosaurs/${dinoId}`);
28
29    if (!response.ok) {
30      throw new APIError(
31        `Error fetching data for dinosaur #${dinoId}`,
32        response.status,
33        `/dinosaurs/${dinoId}`
34      );
35    }
36
37    return await response.json();
38  } catch (error) {
39    if (error instanceof APIError) {
40      // Handle API errors
41      if (error.isAuthError()) {
42        await refreshAuthToken();
43        return fetchDinosaurData(dinoId); // Retry after refreshing the token
44      } else if (error.isServerError()) {
45        console.error('Server error, trying backup API...');
46        return fetchDinosaurDataFromBackupAPI(dinoId);
47      } else {
48        console.error(`Client error: ${error.message} (statusCode: ${error.statusCode})`);
49        throw error; // Re-throw the error
50      }
51    } else {
52      // Other type of error (e.g., network error)
53      console.error('Unexpected error:', error);
54      throw new Error(`Failed to fetch dinosaur #${dinoId} data: ${error.message}`);
55    }
56  }
57}

Advanced Example: Incident Reporting System

Let's see how we can use custom error classes in a more elaborate example — an incident reporting system for Jurassic Park:

1// Base error class for incidents
2class IncidentError extends Error {
3  constructor(message, location, severity) {
4    super(message);
5    this.name = 'IncidentError';
6    this.location = location;
7    this.severity = severity; // 1-10
8    this.timestamp = new Date();
9    this.resolved = false;
10    this.resolutionTime = null;
11  }
12
13  markAsResolved() {
14    this.resolved = true;
15    this.resolutionTime = new Date();
16    return this;
17  }
18
19  getIncidentDuration() {
20    if (!this.resolved) return null;
21    return (this.resolutionTime - this.timestamp) / 1000; // in seconds
22  }
23
24  requiresEvacuation() {
25    return this.severity >= 7;
26  }
27
28  requiresNotification() {
29    return this.severity >= 4;
30  }
31
32  getSummary() {
33    return {
34      type: this.name,
35      message: this.message,
36      location: this.location,
37      severity: this.severity,
38      timestamp: this.timestamp,
39      status: this.resolved ? 'Resolved' : 'Active',
40      resolutionTime: this.resolutionTime
41    };
42  }
43
44  toJSON() {
45    return this.getSummary();
46  }
47}
48
49// Classes for different incident types
50class DinosaurEscapeIncident extends IncidentError {
51  constructor(message, location, species, count, dangerLevel) {
52    super(message, location, dangerLevel);
53    this.name = 'DinosaurEscapeIncident';
54    this.species = species;
55    this.count = count;
56    this.lastSighting = location;
57  }
58
59  updateSighting(newLocation) {
60    this.lastSighting = newLocation;
61    return this;
62  }
63
64  getSummary() {
65    return {
66      ...super.getSummary(),
67      species: this.species,
68      count: this.count,
69      lastSighting: this.lastSighting
70    };
71  }
72}
73
74class SystemFailureIncident extends IncidentError {
75  constructor(message, location, system, affectedAreas, backupStatus) {
76    // Calculate criticality level based on system and backup availability
77    const severity = SystemFailureIncident.calculateSeverity(system, backupStatus);
78
79    super(message, location, severity);
80    this.name = 'SystemFailureIncident';
81    this.system = system;
82    this.affectedAreas = affectedAreas;
83    this.backupStatus = backupStatus;
84  }
85
86  static calculateSeverity(system, backupStatus) {
87    const systemCriticality = {
88      'electric_fences': 10,
89      'security_doors': 9,
90      'surveillance': 7,
91      'climate_control': 6,
92      'guest_services': 3
93    };
94
95    // If backup works, reduce criticality level
96    const baseSeverity = systemCriticality[system] || 5;
97    return backupStatus === 'operational' ? Math.max(3, baseSeverity - 3) : baseSeverity;
98  }
99
100  requiresMaintenanceTeam() {
101    return true;
102  }
103
104  requiresEmergencyPower() {
105    return this.system === 'electric_fences' || this.system === 'security_doors';
106  }
107
108  getSummary() {
109    return {
110      ...super.getSummary(),
111      system: this.system,
112      affectedAreas: this.affectedAreas,
113      backupStatus: this.backupStatus,
114      requiresEmergencyPower: this.requiresEmergencyPower()
115    };
116  }
117}
118
119class VisitorIncident extends IncidentError {
120  constructor(message, location, visitorCount, injuryLevel, medicalStatus) {
121    // Calculate criticality level based on visitor count and injury level
122    const severity = VisitorIncident.calculateSeverity(visitorCount, injuryLevel);
123
124    super(message, location, severity);
125    this.name = 'VisitorIncident';
126    this.visitorCount = visitorCount;
127    this.injuryLevel = injuryLevel; // 'none', 'minor', 'moderate', 'severe'
128    this.medicalStatus = medicalStatus; // 'not_required', 'dispatched', 'on_scene', 'evacuating'
129  }
130
131  static calculateSeverity(visitorCount, injuryLevel) {
132    const injurySeverity = {
133      'none': 1,
134      'minor': 3,
135      'moderate': 6,
136      'severe': 9
137    };
138
139    // Increase criticality level for larger visitor groups
140    const baseSeverity = injurySeverity[injuryLevel] || 5;
141    const visitorFactor = visitorCount > 10 ? 2 : visitorCount > 3 ? 1 : 0;
142
143    return Math.min(10, baseSeverity + visitorFactor);
144  }
145
146  requiresMedicalTeam() {
147    return this.injuryLevel !== 'none';
148  }
149
150  updateMedicalStatus(newStatus) {
151    this.medicalStatus = newStatus;
152
153    // If medical help is on scene and situation is not critical,
154    // we can reduce the criticality level
155    if (newStatus === 'on_scene' && this.injuryLevel !== 'severe') {
156      this.severity = Math.max(2, this.severity - 2);
157    }
158
159    return this;
160  }
161
162  getSummary() {
163    return {
164      ...super.getSummary(),
165      visitorCount: this.visitorCount,
166      injuryLevel: this.injuryLevel,
167      medicalStatus: this.medicalStatus,
168      requiresMedicalTeam: this.requiresMedicalTeam()
169    };
170  }
171}
172
173// Incident management system
174class IncidentManagementSystem {
175  constructor() {
176    this.activeIncidents = new Map();
177    this.resolvedIncidents = [];
178    this.eventListeners = {
179      'new': [],
180      'update': [],
181      'resolve': []
182    };
183  }
184
185  reportIncident(incident) {
186    if (!(incident instanceof IncidentError)) {
187      throw new TypeError('Only IncidentError objects can be reported');
188    }
189
190    const incidentId = `INC-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
191    this.activeIncidents.set(incidentId, incident);
192
193    // Notify all listeners about the new incident
194    this._notifyListeners('new', { id: incidentId, incident });
195
196    // Trigger automated responses
197    this._triggerAutomatedResponses(incidentId, incident);
198
199    return incidentId;
200  }
201
202  updateIncident(incidentId, updateFn) {
203    if (!this.activeIncidents.has(incidentId)) {
204      throw new Error(`Incident with ID ${incidentId} does not exist or has already been resolved`);
205    }
206
207    const incident = this.activeIncidents.get(incidentId);
208    updateFn(incident);
209
210    // Notify all listeners about the incident update
211    this._notifyListeners('update', { id: incidentId, incident });
212
213    return incident;
214  }
215
216  resolveIncident(incidentId, resolutionNotes = '') {
217    if (!this.activeIncidents.has(incidentId)) {
218      throw new Error(`Incident with ID ${incidentId} does not exist or has already been resolved`);
219    }
220
221    const incident = this.activeIncidents.get(incidentId);
222    incident.markAsResolved();
223    incident.resolutionNotes = resolutionNotes;
224
225    // Move incident from active to resolved
226    this.activeIncidents.delete(incidentId);
227    this.resolvedIncidents.push({ id: incidentId, incident });
228
229    // Notify all listeners about the resolution
230    this._notifyListeners('resolve', { id: incidentId, incident });
231
232    return incident;
233  }
234
235  getActiveIncidents() {
236    return Array.from(this.activeIncidents.entries()).map(([id, incident]) => ({
237      id,
238      ...incident.getSummary()
239    }));
240  }
241
242  addEventListener(event, callback) {
243    if (!this.eventListeners[event]) {
244      throw new Error(`Unknown event type: ${event}`);
245    }
246
247    this.eventListeners[event].push(callback);
248    return () => this.removeEventListener(event, callback);
249  }
250
251  removeEventListener(event, callback) {
252    if (!this.eventListeners[event]) return false;
253
254    const index = this.eventListeners[event].indexOf(callback);
255    if (index !== -1) {
256      this.eventListeners[event].splice(index, 1);
257      return true;
258    }
259    return false;
260  }
261
262  _notifyListeners(event, data) {
263    if (!this.eventListeners[event]) return;
264
265    for (const listener of this.eventListeners[event]) {
266      try {
267        listener(data);
268      } catch (error) {
269        console.error(`Error in ${event} event listener:`, error);
270      }
271    }
272  }
273
274  _triggerAutomatedResponses(incidentId, incident) {
275    console.log(`Automated response for incident ${incidentId}:`, incident.name);
276
277    // Different reactions depending on incident type
278    if (incident instanceof DinosaurEscapeIncident) {
279      if (incident.requiresEvacuation()) {
280        console.log(`EVACUATION! Activating evacuation protocol for: ${incident.location}`);
281      }
282
283      console.log(`Dispatching ACU (Asset Containment Unit) to location: ${incident.lastSighting}`);
284    }
285
286    else if (incident instanceof SystemFailureIncident) {
287      console.log(`Notifying maintenance team about system failure: ${incident.system}`);
288
289      if (incident.requiresEmergencyPower()) {
290        console.log('Activating emergency power system!');
291      }
292    }
293
294    else if (incident instanceof VisitorIncident) {
295      if (incident.requiresMedicalTeam()) {
296        console.log(`Dispatching medical team to location: ${incident.location}`);
297
298        // Update medical status
299        this.updateIncident(incidentId, inc => inc.updateMedicalStatus('dispatched'));
300      }
301
302      if (incident.requiresEvacuation()) {
303        console.log(`Starting visitor evacuation from: ${incident.location}`);
304      }
305    }
306
307    // Common reactions for all incidents
308    if (incident.requiresNotification()) {
309      console.log('Notifying park management...');
310    }
311  }
312}
313
314// Example usage of the incident system
315function demoParkIncidentSystem() {
316  const incidentSystem = new IncidentManagementSystem();
317
318  // Add listener for new incidents
319  incidentSystem.addEventListener('new', ({ id, incident }) => {
320    console.log(`NEW INCIDENT [${id}]: ${incident.name} at location ${incident.location}`);
321  });
322
323  // Report dinosaur incident
324  const raptorIncidentId = incidentSystem.reportIncident(
325    new DinosaurEscapeIncident(
326      'Velociraptor escape from enclosure',
327      'Sector C4',
328      'Velociraptor',
329      3,
330      9 // High danger level
331    )
332  );
333
334  // Update incident — change last sighting
335  setTimeout(() => {
336    incidentSystem.updateIncident(raptorIncidentId, incident => {
337      incident.updateSighting('Sector C5 - near visitor center');
338      console.log(`Update: Raptors spotted at ${incident.lastSighting}`);
339    });
340  }, 2000);
341
342  // Report system incident
343  const powerIncidentId = incidentSystem.reportIncident(
344    new SystemFailureIncident(
345      'Electric fence power failure',
346      'Sector B1-B3',
347      'electric_fences',
348      ['TRex Paddock', 'Raptor Containment'],
349      'offline' // Backup not working
350    )
351  );
352
353  // Report visitor incident
354  const visitorIncidentId = incidentSystem.reportIncident(
355    new VisitorIncident(
356      'Visitors outside designated tour route',
357      'Sector D2',
358      4, // Visitor count
359      'minor', // Minor injuries
360      'not_required' // Medical status
361    )
362  );
363
364  // Display active incidents
365  setTimeout(() => {
366    console.log('\n--- ACTIVE INCIDENTS ---');
367    console.table(incidentSystem.getActiveIncidents());
368  }, 3000);
369
370  // Resolve visitor incident
371  setTimeout(() => {
372    incidentSystem.resolveIncident(
373      visitorIncidentId,
374      'Visitors safely escorted back to the designated tour route.'
375    );
376    console.log(`Incident ${visitorIncidentId} marked as resolved.`);
377  }, 4000);
378
379  // Resolve power incident
380  setTimeout(() => {
381    incidentSystem.resolveIncident(
382      powerIncidentId,
383      'Power restored, fences active again.'
384    );
385    console.log(`Incident ${powerIncidentId} marked as resolved.`);
386  }, 5000);
387
388  // Resolve raptor incident
389  setTimeout(() => {
390    incidentSystem.resolveIncident(
391      raptorIncidentId,
392      'All 3 Velociraptors captured and safely transported back to enclosure.'
393    );
394    console.log(`Incident ${raptorIncidentId} marked as resolved.`);
395  }, 6000);
396}
397
398// Run the demo
399// demoParkIncidentSystem();

Best Practices for Creating Custom Error Classes

  1. Name errors meaningfully — the name should clearly indicate what went wrong
  2. Create an error hierarchy — use inheritance to create a logical error class hierarchy
  3. Add context — always attach additional information that will help diagnose and handle the error
  4. Implement helper methods — add methods that make error categorization and handling easier
  5. Ensure serializability — add a
    toJSON()
    method so errors can be easily logged and transmitted
  6. Preserve the stack trace — ensure you call
    super(message)
    in the constructor to preserve the stack trace
  7. Document your error classes — other developers (and future you) should know when and how to use your error classes

Conclusions

Custom error classes are a powerful tool in the JavaScript developer's arsenal. In complex applications like the Jurassic Park management system, precise categorization and handling of different error types is key to maintaining safety and stability.

Remember, as Dr. Ian Malcolm said: "Life... uh... finds a way." The same applies to errors in code. The best approach is not to pretend errors don't exist, but to expect them and be prepared to handle them in the most elegant and effective way possible.

In the next exercise we'll use all the techniques we've learned to implement a complete application demonstrating modules, asynchrony, and error handling in practice.

Go to CodeWorlds