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.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:
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."
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}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}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.
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}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();toJSON() method so errors can be easily logged and transmittedsuper(message) in the constructor to preserve the stack traceCustom 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.