Error class

Introduction

The Error class in JavaScript is a built-in object that provides a way to represent error conditions in a program. It is the base object for error handling and can be used to create user-defined exceptions.

Creating an Error Object

An Error object can be created using the new Error() constructor. It optionally accepts a string argument, which represents the error message.

Example:

javascript
	let myError = new Error('Something went wrong');

Properties of an Error Object

An Error object has two main properties:

  • name: Indicates the type of error (e.g., Error, TypeError, SyntaxError).
  • message: The error message string passed when the error was created.

Example:

javascript
	console.log(myError.name);    // "Error"
	console.log(myError.message); // "Something went wrong"

Throwing Errors

To generate an error, you can use the throw statement. Throwing an error stops the execution of the current function.

Example:

javascript
	if (someCondition) {
	  throw new Error('Error message');
	}

Common Types of Built-in Error Objects

JavaScript includes several built-in error constructors, such as:

  • SyntaxError: An error related to syntax.
  • ReferenceError: An error due to an invalid reference.
  • TypeError: An error indicating a value is not of an expected type.

Custom Error Types

You can also create your own custom error types by extending the Error class. This is useful for creating domain-specific error types in your application.

Example:

javascript
	class MyCustomError extends Error {
	  constructor(message) {
	    super(message); // Call the super constructor
	    this.name = "MyCustomError"; // Set the name property
	  }
	}
	
	// Create an instance of the custom error
	let myError = new MyCustomError('Something went wrong');

Handling Errors

To handle errors, you can use the try...catch statement. The try block contains code that may throw an error, and the catch block contains code to handle the error. This is covered in more detail in the next lesson.

Example:

javascript
	try {
	  // code that may throw an error
	} catch (error) {
	  console.error(error.message);
	}

Best Practices

We will now look at best practices.

Always Provide Meaningful Error Messages

Why It’s Important:

  • Clarity: Clear error messages help developers quickly understand what went wrong.
  • Troubleshooting: They assist in diagnosing and fixing issues efficiently.
  • User Communication: If appropriately filtered, they can inform users about the nature of the issue without exposing technical details or security vulnerabilities.

How to Implement:

  • Descriptive: Include details about the context and cause of the error.
  • Consistent Format: Use a consistent format across your application to make logs easier to parse.
  • Avoid Technical Jargon: If the message is user-facing, avoid technical terms that could confuse the user.

Use try...catch to Handle Errors Gracefully

Why It’s Important:

  • Control Flow: It allows you to manage the flow of the program even when errors occur.
  • Prevents Crashes: Proper use can prevent the entire application from crashing due to unhandled errors.

How to Implement:

  • Scope: Use try...catch blocks around code that might throw errors (e.g., file operations, network requests).
  • Error Segregation: Handle different types of errors differently; e.g., network errors may be handled differently than data validation errors.
  • Nested Try-Catch: In complex operations, nested try...catch blocks can be used to handle errors at different levels of the operation.

Log Errors for Debugging Purposes

Why It’s Important:

  • Tracking: Logs provide a history of when and where errors occurred.
  • Debugging: Detailed logs can significantly shorten the time required to debug and fix issues.

How to Implement:

  • Include Context: Log the state of the program or relevant data when the error occurred.
  • Use Logging Levels: Differentiate between error severity levels (info, warning, error, critical).
  • Centralized Logging: In distributed systems, use a centralized logging system for easier monitoring and analysis.

Consider Custom Errors for Complex Applications

Why It’s Important:

  • Specificity: Custom errors can convey specific issues more precisely than generic errors.
  • Maintainability: They help in organizing error handling logic, especially in large applications.

How to Implement:

  • Extend Error Class: Create custom error classes by extending the native Error class.
  • Add Additional Properties: Include properties that provide extra information about the error (e.g., error codes, contextual data).
  • Group by Functionality: Organize custom errors by application functionality or error source.

Don’t Suppress Errors Without Handling Them Appropriately

Why It’s Important:

  • Avoid Hidden Bugs: Suppressing errors without addressing the underlying issue can lead to hidden bugs that are difficult to trace.
  • Maintain Code Integrity: Proper error handling ensures that the application remains in a known state.

How to Implement:

  • Avoid Empty Catch Blocks: Ensure catch blocks either handle the error or log it for future action.
  • Use Finally for Cleanup: Use finally blocks to clean up resources (like closing file streams), regardless of whether an error occurred.
  • Error Propagation: If an error can’t be handled in the current context, propagate it to a part of the application that can handle it properly.

By adhering to these best practices, developers can ensure that their applications are robust, maintainable, and user-friendly in terms of error management.

Practical examples

For this practical example, we’ll focus on a user login process. This is a common feature in web applications where several things can go wrong, like network issues, incorrect credentials, or server errors. We’ll implement error handling to manage these situations gracefully.

Components:

  1. Login Function: A function to simulate a user login.
  2. Custom Error Types: Define custom error classes for different login errors.
  3. Error Handling: Using try...catch to handle errors and take appropriate actions.

Step 1: Define Custom Error Types

First, let’s define some custom error types for specific login-related issues.

javascript
	class NetworkError extends Error {
	  constructor(message) {
	    super(message);
	    this.name = "NetworkError";
	  }
	}
	
	class AuthenticationError extends Error {
	  constructor(message) {
	    super(message);
	    this.name = "AuthenticationError";
	  }
	}

Step 2: Create a Login Function

Now, let’s create a function that simulates a user login. This function will randomly throw errors to mimic real-world scenarios.

javascript
	async function loginUser(username, password) {
	  try {
	    // Simulate network and authentication errors
	    if (Math.random() < 0.3) {
	      throw new NetworkError("Unable to connect to the server");
	    }
	    if (username !== "user" || password !== "pass") {
	      throw new AuthenticationError("Invalid username or password");
	    }
	
	    // Simulate successful login
	    return "Login successful!";
	  } catch (error) {
	    // Rethrow the error to be handled by the caller
	    throw error;
	  }
	}

Step 3: Handling Errors

Finally, we’ll use the login function and handle any errors that occur.

javascript
	async function performLogin() {
	  try {
	    let response = await loginUser("user", "wrongpass");
	    console.log(response);
	  } catch (error) {
	    if (error instanceof NetworkError) {
	      console.error("Network problem: Please check your internet connection.");
	    } else if (error instanceof AuthenticationError) {
	      console.error("Login failed: Please check your credentials.");
	    } else {
	      console.error("An unexpected error occurred:", error.message);
	    }
	  }
	}
	
	performLogin();

Explanation

  • Custom Errors: NetworkError and AuthenticationError are custom errors to handle specific login issues.
  • Login Function: loginUser function simulates login logic, including random error throwing.
  • Error Handling: In performLogin, errors are caught and handled based on their type. This allows for specific user feedback and logging.

Real-world Considerations

In a real production setting, you would also consider:

  • Logging errors to a server for monitoring and debugging.
  • Providing user-friendly feedback.
  • Implementing retry logic for recoverable errors (like network issues).
  • Security considerations, such as preventing error messages from exposing sensitive information.

Conclusion

In summary, effectively implementing error handling in JavaScript is crucial for developing robust, reliable applications. It involves understanding the Error class, leveraging try…catch blocks for graceful error management, and employing meaningful error messages for clarity. Emphasizing best practices like using custom error types and thorough error logging, while avoiding the suppression of errors, ensures maintainable code and enhances user experience.

Lesson task

Goal

To demonstrate the use of Error class by extending the Error class and throwing a custom error.

Brief

Objective: Create a custom error class and use it in a JavaScript function.

Task Description:

  1. Create a Custom Error Class:

    • Name your class InvalidInputError.
    • It should extend the built-in Error class.
    • The constructor should accept a message string and set the name property to 'InvalidInputError'.
  2. Create a Function Using the Custom Error:

    • Write a function named validateNumber that takes a number as an argument.
    • If the number is not a finite number, throw an instance of InvalidInputError with a relevant error message.
    • If the number is valid, return "Valid number".
  3. Test Your Function:

    • Call validateNumber with both a valid number and an invalid input (like a string) and log the results.

Solution:

javascript
	// Custom Error Class
	class InvalidInputError extends Error {
	  constructor(message) {
	    super(message); // Call the parent class constructor
	    this.name = "InvalidInputError"; // Setting the error name
	  }
	}
	
	// Function using the custom error
	function validateNumber(number) {
	  // Check if the input is not a finite number
	  if (!isFinite(number)) {
	    throw new InvalidInputError("Input must be a valid number");
	  }
	  return "Valid number";
	}
	
	// Testing the function
	try {
	  console.log(validateNumber(42)); // Should log "Valid number"
	  console.log(validateNumber("hello")); // Should throw an error
	} catch (error) {
	  if (error instanceof InvalidInputError) {
	    console.error(error.message); // Log the error message
	  } else {
	    console.error("An unexpected error occurred");
	  }
	}

Comments in the Code:

  • Custom Error Class: InvalidInputError extends the native Error class, allowing for custom error handling.
  • Constructor: Sets up the error message and name specific to InvalidInputError.
  • validateNumber Function: Uses a simple check to determine if the input is a finite number.
  • Error Handling: The try...catch block tests the function, catching and logging errors if they occur.
  • Instance Check: The catch block checks if the caught error is an instance of InvalidInputError, ensuring that only errors of this type are handled here.

This task and solution should provide a practical understanding of how to create and use custom error classes in JavaScript, enhancing error handling capabilities in your code.

Additional resources

MDN Docs: Error