In managing Jurassic Park, where dinosaurs can escape from enclosures, security systems can fail, and visitors can find themselves in danger, proper error handling is critically important. In JavaScript, just as in park safety procedures, we must be prepared for unexpected situations and know how to respond to them.
In this exercise we will focus on error handling strategies in asynchronous code, which is essential for building resilient and reliable applications.
Imagine you are building a dinosaur enclosure monitoring system. What happens if a temperature sensor stops working? Or if the connection to the security system is interrupted? Without proper error handling, your application may "freeze" or, worse, continue operating based on incorrect data.
Good error handling:
Before diving into error handling strategies, let's discuss the different types of errors you can encounter in JavaScript:
Occur when code contains invalid syntax. They are detected during compilation and prevent the code from running.
1// Example of a syntax error
2function fetchDinosaurData( { // Missing closing parenthesis
3 // function body
4}Occur when we try to access a variable or function that does not exist.
1// Example of a reference error
2function updateDinosaurStatus(id) {
3 // The variable 'statuses' has not been defined
4 statuses[id] = "Active";
5}Occur when an operation is performed on a value of the wrong type.
1// Example of a type error
2function countDinosaurs(dinosaurList) {
3 // If dinosaurList is not an array, a TypeError will occur
4 return dinosaurList.length;
5}Occur when a numeric value or parameter is outside the allowable range.
1// Example of a range error
2function setEnclosureTemperature(temperature) {
3 // If temperature is too high, a RangeError is thrown
4 if (temperature > 50) {
5 throw new RangeError("Enclosure temperature cannot exceed 50°C");
6 }
7 // rest of code
8}Occur in case of improper use of URI encoding/decoding functions.
1// Example of a URI error
2try {
3 // Attempt to decode an invalid URI
4 decodeURIComponent('%');
5} catch (error) {
6 console.error("Invalid URI:", error.message);
7}Occurred historically for errors related to the
eval() function, though rarely seen in modern JavaScript.We can create and throw our own errors to handle application-specific cases.
1// Example of a custom error
2class DinosaurEscapeError extends Error {
3 constructor(dinoId, paddockId, message) {
4 super(message || `Dinosaur #${dinoId} escaped from paddock #${paddockId}!`);
5 this.name = "DinosaurEscapeError";
6 this.dinoId = dinoId;
7 this.paddockId = paddockId;
8 this.timestamp = new Date();
9 }
10}
11
12// Usage
13function checkPaddockStatus(paddockId) {
14 const status = fetchPaddockStatus(paddockId);
15
16 if (status.dinosaur && !status.gatesClosed) {
17 throw new DinosaurEscapeError(
18 status.dinosaur.id,
19 paddockId,
20 `ALERT! Dinosaur ${status.dinosaur.name} escaped from paddock #${paddockId}!`
21 );
22 }
23
24 return status;
25}The fundamental error handling mechanism in JavaScript is the
try...catch block:1try {
2 // Code that might throw an error
3 const paddockStatus = checkPaddockStatus(12);
4 updateMonitoringPanel(paddockStatus);
5} catch (error) {
6 // Error handling code
7 console.error("Paddock check error:", error.message);
8 activateAlarm("Paddock monitoring error", error);
9} finally {
10 // This code always runs, regardless of whether an error occurred
11 writeSystemLog("Finished checking paddock 12");
12}In the example above:
try block contains code that might throw an errorcatch block executes when an error occurs in the try blockfinally block always executes, regardless of whether an error occurred or notThe error object passed to the
catch block contains many useful properties:1try {
2 throw new Error("Something went wrong");
3} catch (error) {
4 console.error("Error name:", error.name); // "Error"
5 console.error("Message:", error.message); // "Something went wrong"
6 console.error("Stack trace:", error.stack); // Stack trace showing where the error occurred
7}We often want to react differently to different types of errors:
1try {
2 const result = performRiskyOperation();
3 displayResult(result);
4} catch (error) {
5 if (error instanceof SyntaxError) {
6 console.error("Syntax error:", error.message);
7 } else if (error instanceof ReferenceError) {
8 console.error("Reference error:", error.message);
9 } else if (error instanceof DinosaurEscapeError) {
10 // Trigger emergency procedure!
11 activateSecurityProtocol(error.dinoId, error.paddockId);
12 evacuateVisitors();
13 notifySecurityTeam(error);
14 } else {
15 console.error("Unknown error occurred:", error);
16 }
17}We can throw errors ourselves using the
throw keyword:1function setEnclosureTemperature(enclosureId, temperature) {
2 // Validate arguments
3 if (typeof enclosureId !== 'number' || enclosureId <= 0) {
4 throw new TypeError("Enclosure ID must be a positive number");
5 }
6
7 if (temperature < 10 || temperature > 40) {
8 throw new RangeError("Temperature must be between 10°C and 40°C");
9 }
10
11 // Rest of code that will set the temperature
12 return updateEnclosureTemperature(enclosureId, temperature);
13}
14
15// Usage
16try {
17 setEnclosureTemperature("invalid-id", 25);
18} catch (error) {
19 if (error instanceof TypeError) {
20 console.error("Invalid argument type:", error.message);
21 } else if (error instanceof RangeError) {
22 console.error("Value out of range:", error.message);
23 } else {
24 console.error("Unexpected error:", error.message);
25 }
26}For more advanced error handling, we can create custom error classes that inherit from
Error:1// Error hierarchy for park systems
2class ParkError extends Error {
3 constructor(message) {
4 super(message);
5 this.name = this.constructor.name;
6 // Preserve stack trace
7 if (Error.captureStackTrace) {
8 Error.captureStackTrace(this, this.constructor);
9 }
10 }
11}
12
13class SecuritySystemError extends ParkError {
14 constructor(message, systemId) {
15 super(message || `Security system error #${systemId}`);
16 this.systemId = systemId;
17 }
18}
19
20class FeedingSystemError extends ParkError {
21 constructor(message, paddockId, foodType) {
22 super(message || `Feeding system error in paddock #${paddockId}`);
23 this.paddockId = paddockId;
24 this.foodType = foodType;
25 }
26}
27
28class VisitorSystemError extends ParkError {
29 constructor(message, areaId, visitorCount) {
30 super(message || `Visitor system error in area #${areaId}`);
31 this.areaId = areaId;
32 this.visitorCount = visitorCount;
33 }
34}
35
36// Usage
37function startFeedingSystem(paddockId, foodType) {
38 try {
39 if (!paddockExists(paddockId)) {
40 throw new FeedingSystemError(
41 `Cannot start feeding system: Paddock #${paddockId} does not exist`,
42 paddockId,
43 foodType
44 );
45 }
46
47 // More code...
48
49 } catch (error) {
50 if (error instanceof FeedingSystemError) {
51 // Handle feeding system error specifically
52 notifyFeedingStaff(error);
53 return { success: false, error: error.message, systemType: "feeding" };
54 } else {
55 console.error("Unexpected error:", error);
56 return { success: false, error: "Unknown system error", systemType: "unknown" };
57 }
58 }
59}Error handling in asynchronous code can be more complex, especially when dealing with callbacks, Promises, and async/await functions.
In older async programming styles using callbacks, the "error-first" pattern was common:
1// Error-first style
2function fetchDinosaurData(id, callback) {
3 setTimeout(() => {
4 if (id <= 0) {
5 // First argument is the error (if any)
6 callback(new Error(`Invalid dinosaur ID: ${id}`), null);
7 } else {
8 // First argument is null (no error), second is the result
9 callback(null, { id, name: `Dino-${id}`, species: "Tyrannosaurus" });
10 }
11 }, 1000);
12}
13
14// Usage
15fetchDinosaurData(0, (error, dino) => {
16 if (error) {
17 console.error("Failed to fetch dinosaur data:", error.message);
18 return;
19 }
20
21 console.log("Fetched dinosaur data:", dino);
22});In Promises we use the
.catch() method to handle errors:1function fetchDinosaurData(id) {
2 return new Promise((resolve, reject) => {
3 setTimeout(() => {
4 if (id <= 0) {
5 reject(new Error(`Invalid dinosaur ID: ${id}`));
6 } else {
7 resolve({ id, name: `Dino-${id}`, species: "Tyrannosaurus" });
8 }
9 }, 1000);
10 });
11}
12
13// Usage
14fetchDinosaurData(0)
15 .then(dino => {
16 console.log("Fetched dinosaur data:", dino);
17 })
18 .catch(error => {
19 console.error("Failed to fetch dinosaur data:", error.message);
20 });Promises allow chained error handling:
1fetchDinosaurData(42)
2 .then(dino => {
3 return updateDinosaurStatus(dino.id, "active");
4 })
5 .then(status => {
6 console.log("Status updated:", status);
7 return sendNotification("Dinosaur activated");
8 })
9 .catch(error => {
10 // This catch handles errors from all preceding operations
11 console.error("Error occurred in operation chain:", error.message);
12 });Async/await functions allow using traditional
try...catch syntax even for asynchronous code:1async function activateDinosaur(id) {
2 try {
3 console.log(`Starting activation procedure for dinosaur #${id}...`);
4
5 const dino = await fetchDinosaurData(id);
6 console.log(`Fetched dinosaur data: ${dino.name}`);
7
8 const paddockStatus = await checkPaddockStatus(dino.paddockId);
9 console.log(`Paddock #${dino.paddockId} status: ${paddockStatus.status}`);
10
11 if (!paddockStatus.isSecure) {
12 throw new SecuritySystemError(
13 `Cannot activate dinosaur: Paddock #${dino.paddockId} is not secure`,
14 "paddock-security"
15 );
16 }
17
18 const result = await updateDinosaurStatus(id, "active");
19 console.log(`Dinosaur status updated: ${result.status}`);
20
21 return {
22 success: true,
23 message: `Dinosaur #${id} successfully activated`,
24 dino
25 };
26 } catch (error) {
27 console.error(`Error during activation of dinosaur #${id}:`, error.message);
28
29 if (error instanceof SecuritySystemError) {
30 await activateSecurityAlarm(error.systemId, error.message);
31 }
32
33 return {
34 success: false,
35 message: `Failed to activate dinosaur #${id}`,
36 error: error.message
37 };
38 }
39}This syntax is much more readable than
.then().catch() chains and allows a more natural approach to error handling.For network operations or communication with external systems, it is often worth retrying when a temporary error occurs:
1async function withRetry(operation, options = {}) {
2 const {
3 maxAttempts = 3,
4 delay = 1000,
5 delayMultiplier = 2,
6 logging = true
7 } = options;
8
9 let lastError;
10
11 for (let attempt = 1; attempt <= maxAttempts; attempt++) {
12 try {
13 if (logging && attempt > 1) {
14 console.log(`Attempt ${attempt}/${maxAttempts}...`);
15 }
16
17 return await operation(attempt);
18 } catch (error) {
19 lastError = error;
20
21 if (logging) {
22 console.warn(`Attempt ${attempt}/${maxAttempts} failed:`, error.message);
23 }
24
25 if (attempt < maxAttempts) {
26 const waitTime = delay * Math.pow(delayMultiplier, attempt - 1);
27
28 if (logging) {
29 console.log(`Waiting ${waitTime}ms before next attempt...`);
30 }
31
32 await new Promise(resolve => setTimeout(resolve, waitTime));
33 }
34 }
35 }
36
37 throw lastError;
38}
39
40// Example usage
41async function fetchSensorNodeData(nodeId) {
42 return withRetry(
43 async (attempt) => {
44 console.log(`Attempt ${attempt}: Fetching data from node #${nodeId}...`);
45 const response = await fetch(`https://sensor-api.jurassic-park.com/node/${nodeId}`);
46
47 if (!response.ok) {
48 throw new Error(`HTTP response: ${response.status}`);
49 }
50
51 return await response.json();
52 },
53 {
54 maxAttempts: 5,
55 delay: 2000,
56 delayMultiplier: 1.5
57 }
58 );
59}This pattern prevents repeated attempts at operations that consistently fail:
1class CircuitBreaker {
2 constructor(options = {}) {
3 this.failureThreshold = options.failureThreshold || 3;
4 this.resetTimeout = options.resetTimeout || 30000;
5 this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
6 this.failureCount = 0;
7 this.nextAttempt = Date.now();
8 this.onStateChange = options.onStateChange || (() => {});
9 }
10
11 async execute(fn) {
12 if (this.state === 'OPEN') {
13 if (Date.now() > this.nextAttempt) {
14 this.changeState('HALF_OPEN');
15 } else {
16 throw new Error('Circuit breaker is OPEN');
17 }
18 }
19
20 try {
21 const result = await fn();
22 this.onSuccess();
23 return result;
24 } catch (error) {
25 this.onFailure(error);
26 throw error;
27 }
28 }
29
30 onSuccess() {
31 this.failureCount = 0;
32 if (this.state === 'HALF_OPEN') {
33 this.changeState('CLOSED');
34 }
35 }
36
37 onFailure(error) {
38 this.failureCount += 1;
39 if (this.state === 'HALF_OPEN' || this.failureCount >= this.failureThreshold) {
40 this.changeState('OPEN');
41 }
42 }
43
44 changeState(state) {
45 this.state = state;
46 this.onStateChange(state);
47 if (state === 'OPEN') {
48 this.nextAttempt = Date.now() + this.resetTimeout;
49 }
50 }
51}
52
53// Example usage
54const breaker = new CircuitBreaker({
55 failureThreshold: 3,
56 resetTimeout: 10000,
57 onStateChange: (newState) => {
58 console.log(`Circuit breaker changed state to: ${newState}`);
59 if (newState === 'OPEN') {
60 sendSystemAlert("Sensor system is unavailable");
61 }
62 }
63});
64
65async function fetchSensorData(sensorId) {
66 try {
67 return await breaker.execute(async () => {
68 const response = await fetch(`https://sensor-api.jurassic-park.com/sensor/${sensorId}`);
69 if (!response.ok) {
70 throw new Error(`HTTP Error: ${response.status}`);
71 }
72 return response.json();
73 });
74 } catch (error) {
75 if (error.message === 'Circuit breaker is OPEN') {
76 console.log(`Sensor #${sensorId} unavailable. Using cached data.`);
77 return fetchFromCache(sensorId);
78 }
79 throw error;
80 }
81}This pattern provides an alternative data source or functionality when the primary source fails:
1async function fetchDinosaurDataWithFallback(dinoId) {
2 try {
3 // Try fetching from the primary database
4 return await fetchFromPrimaryDatabase(dinoId);
5 } catch (error) {
6 console.warn(`Failed to fetch from primary database: ${error.message}`);
7
8 try {
9 // Try fetching from the backup database
10 console.log(`Trying backup database...`);
11 return await fetchFromBackupDatabase(dinoId);
12 } catch (fallbackError) {
13 console.warn(`Failed to fetch from backup database: ${fallbackError.message}`);
14
15 try {
16 // Try fetching from local cache
17 console.log(`Trying local cache...`);
18 return await fetchFromLocalCache(dinoId);
19 } catch (cacheError) {
20 console.error(`All data sources failed!`);
21
22 // Return minimal data so the application can still function
23 return {
24 id: dinoId,
25 name: `Unknown Dinosaur #${dinoId}`,
26 species: "Unknown",
27 warning: "Incomplete data — all data sources failed"
28 };
29 }
30 }
31 }
32}This pattern isolates resources into dedicated pools so a failure in one part of the system does not affect other parts:
1class Bulkhead {
2 constructor(options = {}) {
3 this.maxConcurrent = options.maxConcurrent || 10;
4 this.maxQueued = options.maxQueued || 20;
5 this.active = 0;
6 this.queue = [];
7 }
8
9 async execute(fn) {
10 if (this.active >= this.maxConcurrent) {
11 if (this.queue.length >= this.maxQueued) {
12 throw new Error('Bulkhead overflow: Too many requests queued');
13 }
14
15 // Add to queue
16 return new Promise((resolve, reject) => {
17 this.queue.push({ fn, resolve, reject });
18 });
19 }
20
21 this.active += 1;
22
23 try {
24 const result = await fn();
25 return result;
26 } catch (error) {
27 throw error;
28 } finally {
29 this.active -= 1;
30
31 // Handle queued tasks if available
32 if (this.queue.length > 0) {
33 const request = this.queue.shift();
34 this.execute(request.fn).then(request.resolve).catch(request.reject);
35 }
36 }
37 }
38}
39
40// Creating separate bulkheads for different subsystems
41const securityBulkhead = new Bulkhead({ maxConcurrent: 5, maxQueued: 10 });
42const feedingBulkhead = new Bulkhead({ maxConcurrent: 3, maxQueued: 5 });
43const monitoringBulkhead = new Bulkhead({ maxConcurrent: 10, maxQueued: 30 });
44
45// Usage
46async function monitorPaddock(paddockId) {
47 return monitoringBulkhead.execute(async () => {
48 return await fetchMonitoringData(paddockId);
49 });
50}
51
52async function updateSecurity(paddockId, settings) {
53 return securityBulkhead.execute(async () => {
54 return await updateSecuritySettings(paddockId, settings);
55 });
56}In Express/Node.js applications, you can implement middleware for centralized error management:
1// Centralized error handling middleware
2function errorHandler(err, req, res, next) {
3 // Log the error
4 console.error("Error occurred:", err);
5
6 // Check error type
7 if (err instanceof SecuritySystemError) {
8 // Special treatment for security errors
9 activateSecurityProtocol(err);
10 return res.status(500).json({
11 error: "Security system error",
12 code: "SECURITY_FAILURE",
13 message: err.message
14 });
15 } else if (err.name === "ValidationError") {
16 // Validation errors
17 return res.status(400).json({
18 error: "Data validation error",
19 details: err.details,
20 message: err.message
21 });
22 } else if (err.name === "UnauthorizedError") {
23 // Authorization errors
24 return res.status(401).json({
25 error: "Unauthorized",
26 message: "You do not have permission to perform this operation"
27 });
28 }
29
30 // Default error handling
31 res.status(500).json({
32 error: "Internal server error",
33 message: process.env.NODE_ENV === "production" ?
34 "Sorry, something went wrong" : err.message
35 });
36}
37
38// Usage in Express
39app.use(errorHandler);Always handle errors — every async function should have some form of error handling.
Use specific error types — create dedicated error classes for different types of problems.
Provide useful messages — an error should contain information useful for diagnosing the problem.
Log errors centrally — use logging systems that allow centralized collection and analysis of errors.
Never ignore errors — if you catch an error and do not need to handle it further, at least log it.
Preserve the call stack — when throwing new errors, preserve the original call stacks.
1try {
2 await riskyOperation();
3} catch (error) {
4 // Preserving the original call stack
5 throw new Error(`Failed to execute operation: ${error.message}`, { cause: error });
6}Handle errors at the right level — not all errors should be handled in the same place.
Use try...finally — use the
finally block to clean up resources regardless of whether an error occurred.Do not overload with information — do not show users detailed error information that does not concern them.
Prepare for the unexpected — always have a contingency plan for unexpected errors.
Proper error handling is a critical element of building reliable applications. In the context of Jurassic Park, where visitor safety and valuable specimens are at stake, this is even more important.
Good error handling:
Remember that in the world of software, just as in Jurassic Park, it is always better to "expect the unexpected" and be prepared for it.