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-catch
console.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 occurs
try {
console.log("Operation successful");
} catch (error) {
console.log("Error:", error.message);
} finally {
console.log("This always runs");
}
//Output:
//Operation successful
//This always runs

The 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 try

Throwing 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 recommended
try {
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 approach
try {
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 TypeDescription
ErrorGeneric error (base class for all errors)
SyntaxErrorError in the syntax of the code
ReferenceErrorReference to a variable that does not exist
TypeErrorValue is not of the expected type
RangeErrorNumber is outside the allowable range
URIErrorError in encoding or decoding URI
EvalErrorError in the eval() function (rarely used)
//ReferenceError - accessing undefined variable
try {
console.log(undefinedVariable);
} catch (error) {
console.log(error.name); //ReferenceError
console.log(error.message); //undefinedVariable is not defined
}
//TypeError - calling a non-function
try {
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 length
try {
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 null

Custom 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 fetch

Error 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 functions
async 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 handling
function parseJSON(jsonString) {
return JSON.parse(jsonString); //might throw error
}
//good - with error handling
function parseJSON(jsonString) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.log("Invalid JSON:", error.message);
return null;
}
}

2. Provide meaningful error messages

//bad - generic message
throw new Error("Error");
//good - specific message
throw 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 handling
try {
riskyOperation();
} catch (error) {
//do nothing
}
//good - log or handle appropriately
try {
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 caught
async function badExample() {
const data = await fetch("invalid-url"); //error thrown here
}
badExample(); //UnhandledPromiseRejection
//good - error caught
async function goodExample() {
try {
const data = await fetch("invalid-url");
} catch (error) {
console.log("Error:", error.message);
}
}
goodExample();

3. Swallowing errors silently

//bad - error is hidden
try {
riskyOperation();
} catch (error) {
//silently ignoring the error
}
//good - at least log it
try {
riskyOperation();
} catch (error) {
console.error("Operation failed:", error);
}