JavaScript and TypeScript course ยท Module 4: Asynchronous JavaScript
Error Handling (try...catch)
In this lesson8
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.
Why Is Error Handling Important?
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:
- Prevents "cascading failures" - one error should not bring down the entire system
- Provides useful diagnostic information - helps quickly identify and fix problems
- Enables corrective actions - automatic retries, switching to backup systems
- Ensures graceful degradation - even when some features are unavailable, critical systems continue to work
Types of JavaScript Errors
Before diving into error handling strategies, let's discuss the different types of errors you can encounter in JavaScript:
1. Syntax Errors (SyntaxError)
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}2. Reference Errors (ReferenceError)
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}3. Type Errors (TypeError)
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}4. Range Errors (RangeError)
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}5. URI Errors (URIError)
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}6. EvalError
Occurred historically for errors related to the eval() function, though rarely seen in modern JavaScript.
7. Custom Errors
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}Basic Error Handling with try...catch
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:
- The
tryblock contains code that might throw an error - The
catchblock executes when an error occurs in thetryblock - The
finallyblock always executes, regardless of whether an error occurred or not
Error Object Properties
The 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}Checking Error Type
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}Throwing Errors (throw)
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}Creating Custom Error Classes
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
Error handling in asynchronous code can be more complex, especially when dealing with callbacks, Promises, and async/await functions.
Error Handling in Callbacks
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});Error Handling in Promises
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 });Error Handling with async/await
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.
Advanced Error Handling Strategies
1. Retry Pattern
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}2. Circuit Breaker Pattern
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}3. Fallback Pattern
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}4. Bulkhead Pattern
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}5. Error Middleware
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);Error Handling Best Practices
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
finallyblock 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.
Summary
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:
- Ensures the system will operate even in case of problems
- Makes it easier to diagnose and fix problems
- Improves the user experience
- Prevents cascading failures
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.
Code for this lesson: index.js
1// Module Pattern - Module pattern
2console.log("MODULE PATTERN - PARK MANAGEMENT SYSTEM\n");
3
4// Module Pattern - encapsulating data and methods
5const DinosaurPark = (function() {
6 // Private variables and functions
7 let dinosaurs = [];
8 let capacity = 20;
9 let isOpen = false;
10
11 function validateCapacity() {
12 return dinosaurs.length < capacity;
13 }
14
15 function generateId() {
16 return 'DINO-' + Date.now() + '-' + Math.floor(Math.random() * 1000);
17 }
18
19 // Public interface (API)
20 return {
21 addDinosaur: function(species, name) {
22 if (!validateCapacity()) {
23 console.log(`Cannot add ${species}. Park is full! (${dinosaurs.length}/${capacity})`);
24 return false;
25 }
26
27 const dinosaur = {
28 id: generateId(),
29 species: species,
30 name: name,
31 addedAt: new Date().toLocaleTimeString()
32 };
33
34 dinosaurs.push(dinosaur);
35 console.log(`Added: ${species} "${name}" (ID: ${dinosaur.id})`);
36 return true;
37 },
38
39 removeDinosaur: function(id) {
40 const index = dinosaurs.findIndex(d => d.id === id);
41
42 if (index !== -1) {
43 const removed = dinosaurs.splice(index, 1)[0];
44 console.log(`Removed: ${removed.species} "${removed.name}"`);
45 return true;
46 }
47
48 console.log(`No dinosaur found with ID: ${id}`);
49 return false;
50 },
51
52 listDinosaurs: function() {
53 console.log(`\n=== DINOSAUR LIST (${dinosaurs.length}/${capacity}) ===`);
54
55 if (dinosaurs.length === 0) {
56 console.log("Park is empty");
57 return;
58 }
59
60 dinosaurs.forEach((dino, index) => {
61 console.log(`${index + 1}. ${dino.species} "${dino.name}" - ID: ${dino.id}`);
62 });
63 },
64
65 openPark: function() {
66 if (dinosaurs.length === 0) {
67 console.log("Cannot open an empty park!");
68 return false;
69 }
70
71 isOpen = true;
72 console.log(`Park open! Welcome visitors!`);
73 return true;
74 },
75
76 closePark: function() {
77 isOpen = false;
78 console.log(`Park closed.`);
79 },
80
81 getStatus: function() {
82 return {
83 isOpen: isOpen,
84 dinosaurCount: dinosaurs.length,
85 capacity: capacity,
86 occupancy: ((dinosaurs.length / capacity) * 100).toFixed(1) + '%'
87 };
88 }
89 };
90})();
91
92// Using the module
93console.log("=== ADDING DINOSAURS ===\n");
94
95DinosaurPark.addDinosaur('T-Rex', 'Rexy');
96DinosaurPark.addDinosaur('Velociraptor', 'Blue');
97DinosaurPark.addDinosaur('Triceratops', 'Trike');
98
99DinosaurPark.listDinosaurs();
100
101console.log("\n=== PARK STATUS ===\n");
102console.log(DinosaurPark.getStatus());
103
104console.log("\n=== OPENING THE PARK ===\n");
105DinosaurPark.openPark();
106
107// Attempt to access private variables
108// console.log(dinosaurs); // ERROR! dinosaurs is private
109// console.log(capacity); // ERROR! capacity is privateSpotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. How do you handle an error in an async function?
Hands-on tasks in the game
- Code editor
Write an async function with a try/catch block that attempts to fetch data from a sensor and gracefully handles connection errors.
- Vertical ordering
Arrange the stages of a Promise lifecycle: