Javascript 101
1.7 Error Handling
1.7 Error Handling
Introduction
When writing programs, errors are inevitable. They can occur due to invalid user input, network failures, or bugs in the code. Error handling allows us to gracefully handle these errors and prevent our program from crashing unexpectedly.
In Javascript, we use the try-catch-finally statement to handle errors. This is similar to how error handling works in Java and other programming languages.
Try-Catch Statement
The try block contains code that might throw an error. If an error occurs, the program will immediately jump to the catch block. If no error occurs, the catch block is skipped entirely.
try { //code that might throw an error console.log("Starting..."); throw new Error("Something went wrong!"); console.log("This will not be executed");} catch (error) { //code to handle the error console.log("Error occurred:", error.message);}
console.log("Program continues...");
//Output://Starting...//Error occurred: Something went wrong!//Program continues...Without the try-catch statement, an error would cause the entire program to stop execution.
//without try-catchconsole.log("Starting...");throw new Error("Something went wrong!");console.log("This will not be executed");console.log("Program continues...");
//Output://Starting...//Uncaught Error: Something went wrong!//(program stops here)The Error Object
When an error is caught, the catch block receives an Error object which contains information about the error. The Error object has several useful properties:
try { throw new Error("Something went wrong!");} catch (error) { console.log(error.name); //Error console.log(error.message); //Something went wrong! console.log(error.stack); //stack trace showing where the error occurred}The stack property is particularly useful for debugging as it shows the call stack at the point where the error was thrown.
Finally Block
The finally block is optional and will always execute regardless of whether an error occurred or not. This is useful for cleanup operations such as closing file handles or database connections.
try { console.log("Attempting operation..."); throw new Error("Operation failed");} catch (error) { console.log("Error:", error.message);} finally { console.log("Cleanup operations");}
//Output://Attempting operation...//Error: Operation failed//Cleanup operations//finally runs even when no error occurstry { console.log("Operation successful");} catch (error) { console.log("Error:", error.message);} finally { console.log("This always runs");}
//Output://Operation successful//This always runsThe finally block executes even if the try or catch block contains a return statement.
function testFinally() { try { console.log("Try block"); return "Returning from try"; } finally { console.log("Finally block"); }}
console.log(testFinally());
//Output://Try block//Finally block//Returning from tryThrowing Errors
We can manually throw errors using the throw keyword. This is useful when we want to signal that something went wrong in our code.
function divide(a, b) { if (b === 0) { throw new Error("Cannot divide by zero"); } return a / b;}
try { console.log(divide(10, 2)); //5 console.log(divide(10, 0)); //throws error} catch (error) { console.log("Error:", error.message); //Error: Cannot divide by zero}We can throw any value in Javascript, not just Error objects. However, it is best practice to throw Error objects as they provide more information for debugging.
//possible but not recommendedtry { throw "This is an error string";} catch (error) { console.log(error); //This is an error string console.log(error.stack); //undefined (no stack trace)}
//better approachtry { throw new Error("This is an error object");} catch (error) { console.log(error.message); //This is an error object console.log(error.stack); //stack trace available}Built-in Error Types
Javascript provides several built-in error types that are more specific than the generic Error object:
| Error Type | Description |
|---|---|
Error | Generic error (base class for all errors) |
SyntaxError | Error in the syntax of the code |
ReferenceError | Reference to a variable that does not exist |
TypeError | Value is not of the expected type |
RangeError | Number is outside the allowable range |
URIError | Error in encoding or decoding URI |
EvalError | Error in the eval() function (rarely used) |
//ReferenceError - accessing undefined variabletry { console.log(undefinedVariable);} catch (error) { console.log(error.name); //ReferenceError console.log(error.message); //undefinedVariable is not defined}//TypeError - calling a non-functiontry { const obj = {}; obj.someMethod(); //obj.someMethod is not a function} catch (error) { console.log(error.name); //TypeError console.log(error.message); //obj.someMethod is not a function}//RangeError - invalid array lengthtry { const arr = new Array(-1); //negative length not allowed} catch (error) { console.log(error.name); //RangeError console.log(error.message); //Invalid array length}We can throw these specific error types in our own code:
function processAge(age) { if (typeof age !== "number") { throw new TypeError("Age must be a number"); } if (age < 0 || age > 150) { throw new RangeError("Age must be between 0 and 150"); } console.log("Age is valid:", age);}
try { processAge("twenty"); //throws TypeError} catch (error) { console.log(error.name, "-", error.message); //TypeError - Age must be a number}
try { processAge(200); //throws RangeError} catch (error) { console.log(error.name, "-", error.message); //RangeError - Age must be between 0 and 150}Catching Specific Error Types
We can check the type of error in the catch block and handle different errors differently:
function riskyOperation(value) { if (value === null) { throw new TypeError("Value cannot be null"); } if (value < 0) { throw new RangeError("Value must be positive"); } return value * 2;}
try { riskyOperation(null);} catch (error) { if (error instanceof TypeError) { console.log("Type error:", error.message); } else if (error instanceof RangeError) { console.log("Range error:", error.message); } else { console.log("Unknown error:", error.message); }}
//Output://Type error: Value cannot be nullCustom Error Classes
We can create our own custom error types by extending the Error class. This is useful for creating more specific error types for our application.
class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; }}
class DatabaseError extends Error { constructor(message) { super(message); this.name = "DatabaseError"; }}
function validateUser(user) { if (!user.name) { throw new ValidationError("User name is required"); } if (!user.email) { throw new ValidationError("User email is required"); }}
try { validateUser({ name: "John" }); //missing email} catch (error) { if (error instanceof ValidationError) { console.log("Validation failed:", error.message); //Validation failed: User email is required } else if (error instanceof DatabaseError) { console.log("Database error:", error.message); } else { console.log("Unknown error:", error.message); }}Error Handling with Promises
When working with Promises (see Promises), we can handle errors using the catch() method:
function fetchData(url) { return fetch(url) .then((response) => { if (!response.ok) { throw new Error("Network response was not ok"); } return response.json(); }) .catch((error) => { console.log("Fetch error:", error.message); });}
fetchData("https://invalid-url-that-does-not-exist.com");//Fetch error: Failed to fetchError Handling with Async-Await
When using async-await syntax (see Async-Await), we can use try-catch blocks just like with synchronous code:
async function getData() { try { const response = await fetch("https://jsonplaceholder.typicode.com/users/1"); const data = await response.json(); console.log(data); } catch (error) { console.log("Error fetching data:", error.message); }}
getData();//handling specific errors in async functionsasync function processData(id) { if (!id) { throw new ValidationError("ID is required"); }
try { const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); return data; } catch (error) { if (error instanceof ValidationError) { console.log("Validation error:", error.message); } else { console.log("Fetch error:", error.message); } throw error; //re-throw the error if needed }}Best Practices
1. Always catch errors for operations that might fail
//bad - no error handlingfunction parseJSON(jsonString) { return JSON.parse(jsonString); //might throw error}
//good - with error handlingfunction parseJSON(jsonString) { try { return JSON.parse(jsonString); } catch (error) { console.log("Invalid JSON:", error.message); return null; }}2. Provide meaningful error messages
//bad - generic messagethrow new Error("Error");
//good - specific messagethrow new Error("Failed to save user: email already exists");3. Clean up resources in the finally block
let file = null;
try { file = openFile("data.txt"); processFile(file);} catch (error) { console.log("Error processing file:", error.message);} finally { if (file) { file.close(); //always close the file }}4. Don't catch errors unless you can handle them
//bad - catching but not handlingtry { riskyOperation();} catch (error) { //do nothing}
//good - log or handle appropriatelytry { riskyOperation();} catch (error) { console.error("Operation failed:", error); //provide fallback behavior useDefaultValue();}5. Re-throw errors when appropriate
Sometimes we want to catch an error, do some logging or cleanup, but still propagate the error to the caller.
function processData(data) { try { return expensiveOperation(data); } catch (error) { console.error("Error in processData:", error); //log the error but still throw it throw error; }}
try { processData(someData);} catch (error) { console.log("Caught in outer handler:", error.message);}Common Pitfalls
1. Forgetting that errors stop execution
try { console.log("Step 1"); throw new Error("Something went wrong"); console.log("Step 2"); //this will NOT execute} catch (error) { console.log("Error caught");}2. Not handling async errors properly
//bad - error not caughtasync function badExample() { const data = await fetch("invalid-url"); //error thrown here}
badExample(); //UnhandledPromiseRejection
//good - error caughtasync function goodExample() { try { const data = await fetch("invalid-url"); } catch (error) { console.log("Error:", error.message); }}
goodExample();3. Swallowing errors silently
//bad - error is hiddentry { riskyOperation();} catch (error) { //silently ignoring the error}
//good - at least log ittry { riskyOperation();} catch (error) { console.error("Operation failed:", error);}