Basic Error Handling with try...catch

The try...catch statement allows you to handle exceptions gracefully. The code inside the try block is executed, and if an error occurs, control is transferred to the catch block.

Example 1:
try {
    // Code that may throw an error
    let result = someFunction();
    console.log(result);
} catch (error) {
    // Code to handle the error
    console.error('An error occurred:', error.message);
}
Example 2: Handling a Reference Error
try {
    let result = someUndefinedFunction();
    console.log(result);
} catch (error) {
    console.error('Caught a reference error:', error.message);
}
Example 3: Handling a Type Error
try {
    let obj = null;
    obj.someMethod();
} catch (error) {
    console.error('Caught a type error:', error.message);
}
Example 4: Handling a Syntax Error
try {
    eval('var a = ;');
} catch (error) {
    console.error('Caught a syntax error:', error.message);
}

Throwing Custom Errors

You can throw custom errors using the throw statement. This is useful for creating more descriptive error messages or custom error types.

Example 1:
function checkAge(age) {
    if (age < 18) {
        throw new Error('You must be at least 18 years old.');
    }
    return true;
}

try {
    checkAge(15);
} catch (error) {
    console.error(error.message); // Output: You must be at least 18 years old.
}

Example 2: Invalid Parameter Error
function divide(a, b) {
    if (b === 0) {
        throw new Error('Division by zero is not allowed.');
    }
    return a / b;
}

try {
    divide(10, 0);
} catch (error) {
    console.error(error.message); // Output: Division by zero is not allowed.
}

Example 3: Custom Error Class

class NotFoundError extends Error {
    constructor(message) {
        super(message);
        this.name = 'NotFoundError';
    }
}

function findItem(items, id) {
    const item = items.find(item => item.id === id);
    if (!item) {
        throw new NotFoundError(`Item with ID ${id} not found.`);
    }
    return item;
}

try {
    findItem([], 1);
} catch (error) {
    console.error(error.message); // Output: Item with ID 1 not found.
}
Example 4: Invalid Argument Error
function calculateSquareRoot(number) {
    if (number < 0) {
        throw new Error('Cannot calculate the square root of a negative number.');
    }
    return Math.sqrt(number);
}

try {
    calculateSquareRoot(-1);
} catch (error) {
    console.error(error.message); // Output: Cannot calculate the square root of a negative number.
}

The finally Block

The finally block contains code that will run regardless of whether an error was thrown or not. It’s typically used for cleanup operations.

Example 1:
try {
    let data = fetchData();
} catch (error) {
    console.error('Failed to fetch data:', error);
} finally {
    console.log('Cleanup operations');
}
Example 2: Logging Cleanup
try {
    let data = fetchData();
} catch (error) {
    console.error('Failed to fetch data:', error);
} finally {
    console.log('Cleanup operations'); // Always executed
}
Example 3: Closing Resources
function processFile(file) {
    try {
        let data = readFile(file);
        // Process data
    } catch (error) {
        console.error('Error processing file:', error);
    } finally {
        closeFile(file); // Ensure the file is closed
    }
}

processFile('example.txt');
Example 4: Resetting State
let state = { status: 'in-progress' };

try {
    performOperation();
} catch (error) {
    console.error('Operation failed:', error);
} finally {
    state.status = 'idle'; // Reset state
    console.log('State reset to idle');
}

Nested try...catch Blocks

Sometimes, you may need to have nested try...catch blocks for more granular error handling.

Example 1:
try {
    let data = fetchData();
    try {
        process(data);
    } catch (processingError) {
        console.error('Error during processing:', processingError);
    }
} catch (fetchError) {
    console.error('Error fetching data:', fetchError);
}
Example 2: Separate Handling for Different Operations
try {
    let data = fetchData();
    try {
        process(data);
    } catch (processingError) {
        console.error('Error during processing:', processingError);
    }
} catch (fetchError) {
    console.error('Error fetching data:', fetchError);
}
Example 3: Network and Parsing Errors
try {
    let response = fetch('https://api.example.com/data');
    try {
        let data = JSON.parse(response);
    } catch (parsingError) {
        console.error('Error parsing JSON:', parsingError);
    }
} catch (networkError) {
    console.error('Network error:', networkError);
}
Example 4: File Reading and Parsing Errors
try {
    let fileContent = readFile('data.txt');
    try {
        let jsonData = JSON.parse(fileContent);
    } catch (jsonError) {
        console.error('JSON parsing error:', jsonError);
    }
} catch (fileError) {
    console.error('File reading error:', fileError);
}

Best Practices

  1. Always Catch Errors: Ensure every potential error source is wrapped in a try...catch block.
  2. Be Specific: Catch specific errors if possible rather than a generic error.
  3. Avoid Silent Failures: Always log or handle the error in some way to avoid silent failures.
  4. Use Custom Errors: Define custom error types for better error handling
  5. Clean Up: Use the finally block for cleanup tasks that need to run regardless of success or failure
  6. Avoid Overusing Try-Catch: Use error handling judiciously. Overusing try...catch can make code harder to read and maintain.

Advanced Error Handling with Custom Error Types

Creating custom error types can make error handling more descriptive and manageable

Example 1:
class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = 'ValidationError';
    }
}

function validateUser(user) {
    if (!user.name) {
        throw new ValidationError('Name is required.');
    }
    if (!user.age) {
        throw new ValidationError('Age is required.');
    }
}

try {
    validateUser({ age: 25 });
} catch (error) {
    if (error instanceof ValidationError) {
        console.error('Validation Error:', error.message);
    } else {
        console.error('Unknown Error:', error);
    }
}
Example 2: Custom Validation Error
class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = 'ValidationError';
    }
}

function validateEmail(email) {
    if (!email.includes('@')) {
        throw new ValidationError('Invalid email format.');
    }
}

try {
    validateEmail('invalidEmail');
} catch (error) {
    if (error instanceof ValidationError) {
        console.error('Validation Error:', error.message);
    } else {
        console.error('Unknown Error:', error);
    }
}
Example 3: Custom Authentication Error
class AuthenticationError extends Error {
    constructor(message) {
        super(message);
        this.name = 'AuthenticationError';
    }
}

function authenticateUser(user) {
    if (!user.isAuthenticated) {
        throw new AuthenticationError('User is not authenticated.');
    }
}

try {
    authenticateUser({ is
Example 4: Custom Database Error
class DatabaseError extends Error {
    constructor(message) {
        super(message);
        this.name = 'DatabaseError';
    }
}

function fetchDataFromDatabase() {
    // Simulating a database error
    throw new DatabaseError('Failed to connect to the database.');
}

try {
    fetchDataFromDatabase();
} catch (error) {
    if (error instanceof DatabaseError) {
        console.error('Database Error:', error.message);
    } else {
        console.error('Unknown Error:', error);
    }
}

Error Handling in Asynchronous Code

Handling errors in asynchronous code, such as with Promises or async/await, requires a different approach.

Sure, let’s dive into JavaScript error handling from beginner to advanced concepts. Error handling is crucial for making robust and maintainable applications. We’ll start with the basics and gradually move to more complex techniques.

Basic Error Handling with try...catch

The try...catch statement allows you to handle exceptions gracefully. The code inside the try block is executed, and if an error occurs, control is transferred to the catch block.

Example:

javascriptCopy codetry {
    // Code that may throw an error
    let result = someFunction();
    console.log(result);
} catch (error) {
    // Code to handle the error
    console.error('An error occurred:', error.message);
}

In this example, if someFunction() throws an error, the catch block will catch it and log the error message.

Throwing Custom Errors

You can throw custom errors using the throw statement. This is useful for creating more descriptive error messages or custom error types.

Example:

javascriptCopy codefunction checkAge(age) {
    if (age < 18) {
        throw new Error('You must be at least 18 years old.');
    }
    return true;
}

try {
    checkAge(15);
} catch (error) {
    console.error(error.message); // Output: You must be at least 18 years old.
}

The finally Block

The finally block contains code that will run regardless of whether an error was thrown or not. It’s typically used for cleanup operations.

Example:

javascriptCopy codetry {
    let data = fetchData();
} catch (error) {
    console.error('Failed to fetch data:', error);
} finally {
    console.log('Cleanup operations');
}

Nested try...catch Blocks

Sometimes, you may need to have nested try...catch blocks for more granular error handling.

Example:

javascriptCopy codetry {
    let data = fetchData();
    try {
        process(data);
    } catch (processingError) {
        console.error('Error during processing:', processingError);
    }
} catch (fetchError) {
    console.error('Error fetching data:', fetchError);
}

Best Practices

  1. Always Catch Errors: Ensure every potential error source is wrapped in a try...catch block.
  2. Be Specific: Catch specific errors if possible rather than a generic error.
  3. Avoid Silent Failures: Always log or handle the error in some way to avoid silent failures.
  4. Use Custom Errors: Define custom error types for better error handling.
  5. Clean Up: Use the finally block for cleanup tasks that need to run regardless of success or failure.
  6. Avoid Overusing Try-Catch: Use error handling judiciously. Overusing try...catch can make code harder to read and maintain.

Advanced Error Handling with Custom Error Types

Creating custom error types can make error handling more descriptive and manageable.

Example:

Error Handling in Asynchronous Code

Handling errors in asynchronous code, such as with Promises or async/await, requires a different approach.

Example 1: Promise-based Fetch with Error Handling
fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error('Error fetching data:', error));
Example 2: Async/Await with Try-Catch
async function fetchData() {
    try {
        let response = await fetch('https://api.example.com/data');
        let data = await response.json();
        console.log(data);
    } catch (error) {
        console.error('Error fetching data:', error);
    }
}

fetchData();
Example 3: Handling Multiple Asynchronous Operations
async function fetchAndProcessData() {
    try {
        let response = await fetch('https://api.example.com/data');
        let data = await response.json();
        try {
            processData(data);
        } catch (processingError) {
            console.error('Error processing data:', processingError);
        }
    } catch (fetchError) {
        console.error('Error fetching data:', fetchError);
    }
}

fetchAndProcessData();
Example 4: Handling Async Function Inside a Try-Catch
async function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => reject('Network error'), 1000);
    });
}

(async () => {
    try {
        let data = await fetchData();
        console.log(data);
    } catch (error) {
        console.error('Caught async error:', error);
    }
})();
Example 5: Sequential Asynchronous Operations
async function fetchData(url) {
    let response = await fetch(url);
    if (!response.ok) {
        throw new Error('Network response was not ok');
    }
    return await response.json();
}

async function main() {
    try {
        let userData = await fetchData('https://api.example.com/user');
        let ordersData = await fetchData(`https://api.example.com/orders?user=${userData.id}`);
        console.log('User data:', userData);
        console.log('Orders data:', ordersData);
    } catch (error) {
        console.error('Error in fetching data:', error);
    }
}

main();
Example 6: Error Handling with Promise all
async function fetchMultipleData() {
    const urls = ['https://api.example.com/data1', 'https://api.example.com/data2'];

    try {
        let results = await Promise.all(urls.map(url => fetch(url).then(res => res.json())));
        console.log('Results:', results);
    } catch (error) {
        console.error('Error fetching multiple data:', error);
    }
}

fetchMultipleData();

Standard Coding Structure for Error Handling

A standard structure for robust error handling might look like this:

  1. Try to execute the main logic.
  2. Catch any errors and handle them appropriately.
  3. Perform any necessary cleanup.
Example 1:
async function mainFunction() {
    try {
        let data = await fetchData();
        process(data);
    } catch (error) {
        if (error instanceof ValidationError) {
            console.error('Validation Error:', error.message);
        } else {
            console.error('Unexpected Error:', error);
        }
    } finally {
        console.log('Executing cleanup tasks');
    }
}

mainFunction();
Example 2: Centralized Error Handling Function
function handleError(error) {
    if (error instanceof ValidationError) {
        console.error('Validation Error:', error.message);
    } else if (error instanceof AuthenticationError) {
        console.error('Authentication Error:', error.message);
    } else {
        console.error('Unexpected Error:', error.message);
    }
}

async function mainFunction() {
    try {
        let data = await fetchData();
        process(data);
    } catch (error) {
        handleError(error);
    } finally {
        console.log('Executing cleanup tasks');
    }
}

mainFunction();
Example 3: Using Custom Error Types for Better Clarity
class CustomError extends Error {
    constructor(name, message) {
        super(message);
        this.name = name;
    }
}

function handleCustomError(error) {
    switch (error.name) {
        case 'ValidationError':
            console.error('Validation Error:', error.message);
            break;
        case 'AuthenticationError':
            console.error('Authentication Error:', error.message);
            break;
        case 'DatabaseError':
            console.error('Database Error:', error.message);
            break;
        default:
            console.error('Unknown Error:', error.message);
    }
}

async function mainFunction() {
    try {
        let data = await fetchData();
        process(data);
    } catch (error) {
        handleCustomError(error);
    } finally {
        console.log('Executing cleanup tasks');
    }
}

mainFunction();
Example 4: Comprehensive Error Handling Strategy
async function fetchData() {
    // Simulate data fetching
    throw new CustomError('NetworkError', 'Failed to fetch data');
}

async function process(data) {
    if (!data) {
        throw new CustomError('DataError', 'No data provided');
    }
    // Process data
}

async function mainFunction() {
    try {
        let data = await fetchData();
        await process(data);
    } catch (error) {
        if (error instanceof CustomError) {
            console.error(`${error.name}: ${error.message}`);
        } else {
            console.error('Unexpected Error:', error);
        }
    } finally {
        console.log('Executing cleanup tasks');
    }
}

mainFunction();

Summary

By following these guidelines and examples, you can effectively handle errors in JavaScript, making your applications more robust and easier to maintain. Error handling is a critical skill for any developer, and mastering it will help you build better, more reliable software.

Interview Questions:-

here are 20 interview questions and answers based on JavaScript error handling:
1. What is the purpose of the try...catch statement in JavaScript?
  • Answer: The try...catch statement is used to handle exceptions gracefully. Code inside the try block is executed, and if an error occurs, control is transferred to the catch block where the error can be handled.
2. What happens if an error occurs inside a try block?
  • Answer: If an error occurs inside a try block, the control is immediately transferred to the catch block, and the code inside the catch block is executed. The code after the try...catch statement is then executed.
3. How do you throw an error in JavaScript?
  • Answer: You can throw an error using the throw statement. For example: throw new Error('Something went wrong!');
4. What is the purpose of the finally block?
  • Answer: The finally block contains code that will always run regardless of whether an error was thrown or not. It’s typically used for cleanup operations.
5. What is the output of the following code?
try {
    let result = 10 / 0;
    console.log(result);
} catch (error) {
    console.error('An error occurred:', error.message);
} finally {
    console.log('Cleanup operations');
}
  • Answer: The output will be:
Infinity
Cleanup operations
6. How can you create a custom error in JavaScript?
  • Answer: You can create a custom error by extending the Error class. For example
class CustomError extends Error {
    constructor(message) {
        super(message);
        this.name = 'CustomError';
    }
}
7. Why would you use custom error types?
  • Answer: Custom error types allow you to create more descriptive and specific error messages, making it easier to handle different kinds of errors in a more granular way.
8. What is the output of the following code?
class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = 'ValidationError';
    }
}

try {
    throw new ValidationError('Invalid input');
} catch (error) {
    console.error(error.name + ': ' + error.message);
}
  • Answer: The output will be:
ValidationError: Invalid input
9. How do you handle errors in a promise chain?
  • Answer: You handle errors in a promise chain using the .catch() method. For example
fetchData()
    .then(data => process(data))
    .catch(error => console.error('Error:', error));
10.How do you handle errors with async/await?
  • Answer: You handle errors with async/await using a try...catch block. For example
async function fetchData() {
    try {
        let response = await fetch('https://api.example.com/data');
        let data = await response.json();
        console.log(data);
    } catch (error) {
        console.error('Error fetching data:', error);
    }
}

fetchData();
11. What is the output of the following code?
async function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => reject('Network error'), 1000);
    });
}

(async () => {
    try {
        let data = await fetchData();
        console.log(data);
    } catch (error) {
        console.error('Caught async error:', error);
    }
})();
  • Answer: The output will be
Caught async error: Network error
12. Why would you use nested try...catch blocks?
  • Answer: Nested try...catch blocks are used for more granular error handling, allowing you to handle different errors separately at different levels of your code.
13. What is the output of the following code?
try {
    let data = fetchData();
    try {
        process(data);
    } catch (processingError) {
        console.error('Error during processing:', processingError);
    }
} catch (fetchError) {
    console.error('Error fetching data:', fetchError);
}
  • Answer: The output depends on whether fetchData or process throws an error. If fetchData throws an error, it will log Error fetching data: .... If process throws an error, it will log Error during processing: ....
14. What is a common best practice for error handling in JavaScript?
  • Answer: A common best practice is to always catch errors and handle them appropriately, providing meaningful error messages and ensuring cleanup operations are performed.
15. Why is it important to avoid silent failures?
  • Answer: Silent failures make it difficult to debug and maintain the code, as errors go unnoticed. Always logging or handling errors helps in identifying and resolving issues promptly.
16. How can you ensure that a resource is cleaned up regardless of whether an error occurs?
  • Answer: You can ensure that a resource is cleaned up by using the finally block. For example:
try {
    // Code that may throw an error
} catch (error) {
    // Handle error
} finally {
    // Cleanup code
}
17. What is the purpose of the instanceof operator in error handling?
  • Answer: The instanceof operator is used to check if an error is an instance of a specific error type, allowing you to handle different error types differently.
18. What is the output of the following code?
class DatabaseError extends Error {
    constructor(message) {
        super(message);
        this.name = 'DatabaseError';
    }
}

try {
    throw new DatabaseError('Connection failed');
} catch (error) {
    if (error instanceof DatabaseError) {
        console.error('Database Error:', error.message);
    } else {
        console.error('Unknown Error:', error);
    }
}
  • Answer: The output will be:
Database Error: Connection failed
19. How can you handle multiple errors in an asynchronous function?
  • Answer: You can handle multiple errors in an asynchronous function by using multiple try...catch blocks or by handling errors at different stages of the asynchronous operations. For example:
async function fetchAndProcessData() {
    try {
        let data = await fetchData();
        try {
            process(data);
        } catch (processingError) {
            console.error('Error during processing:', processingError);
        }
    } catch (fetchError) {
        console.error('Error fetching data:', fetchError);
    }
}

fetchAndProcessData();
20. How do you handle errors when using Promise all?
  • Answer: You handle errors with Promise all by attaching a .catch() method to the returned promise. For example:
async function fetchMultipleData() {
    const urls = ['https://api.example.com/data1', 'https://api.example.com/data2'];

    try {
        let results = await Promise.all(urls.map(url => fetch(url).then(res => res.json())));
        console.log('Results:', results);
    } catch (error) {
        console.error('Error fetching multiple data:', error);
    }
}

fetchMultipleData();

These questions and answers cover a wide range of topics in JavaScript error handling, from basic concepts to advanced techniques, ensuring a comprehensive understanding of error handling practices.