JavaScript Error Handling - Understanding try-catch, finally, and Handling Rejected Promises

This comprehensive guide covers JavaScript's error handling mechanisms, including the `try-catch` construct, the `finally` block, and how to handle rejected promises using the `catch` and `finally` methods. Written in a beginner-friendly manner with detailed explanations and examples.

Introduction to Error Handling

Welcome to this in-depth guide on JavaScript error handling! Error handling is a critical aspect of programming that can greatly improve the robustness and user experience of your applications. By understanding how to handle errors, you can ensure that your code not only performs its intended function but also responds gracefully to unexpected situations.

Understanding Errors in JavaScript

Before diving into the how of error handling, it’s important to understand what an error is in JavaScript and the different types of errors you might encounter.

What is an Error?

An error occurs when something goes wrong during the execution of a program. In JavaScript, errors can be thrown explicitly by the code or can occur due to runtime issues. For example, trying to access a property on undefined will result in a runtime error.

Errors can break your code and make it difficult to debug, especially if they’re not handled properly. That’s where error handling comes into play.

Types of Errors

There are several categories of errors in JavaScript, including:

  • Syntax Errors: These occur when the code you write does not conform to the rules of the language. For example, forgetting a closing parenthesis or misspelling a keyword.
  • Runtime Errors: These happen during the execution of the program, such as trying to access an object property that doesn’t exist.
  • Logical Errors: These aren’t detected as syntax errors and don’t crash your program, but they cause the program to behave in unexpected ways. For instance, an infinite loop due to incorrect loop conditions.

Using try-catch for Synchronous Code

The try-catch construct is one of the fundamental ways to handle errors in JavaScript, primarily used for synchronous code. Let’s explore how it works.

The try Block

The try block lets you test a block of code for errors.

What is the try Block?

The try block contains the code that you want to run and monitor for errors. If an error occurs within the try block, the control flow is immediately passed to the catch block, skipping the rest of the code inside the try block.

Writing the try Block

Here’s a simple example to illustrate the try block:

try {
    // Code to try
    let result = 10 / 0;
    console.log("This will not be executed if an error occurs above");
} catch (error) {
    console.error("An error occurred:", error.message);
}

In this example, the division by zero will raise an error, and the code execution will jump to the catch block.

The catch Block

The catch block is used to handle errors that were thrown in the try block.

What is the catch Block?

The catch block is where you define what should happen if an error occurs in the try block. The catch block receives the error that was thrown and can use it to deal with the situation appropriately.

Writing the catch Block

Here’s how you can write a catch block to handle errors:

try {
    // Code to try
    let result = 10 / 0;
} catch (error) {
    console.error("An error occurred:", error.message);
}
  • catch (error): The catch block takes an error object as a parameter, which contains information about the error.
  • error.message: The message property of the error object provides a human-readable error message.

Accessing the Error Object

The error object passed to the catch block contains useful information about the error, such as:

  • message: A human-readable description of the error.
  • name: The name of the error, like "TypeError" or "ReferenceError".
  • stack: The call stack when the error occurred, which can help in debugging.

Here’s an example demonstrating accessing the error object:

try {
    let result = 10 / 0;
} catch (error) {
    console.error("Error Name:", error.name);
    console.error("Error Message:", error.message);
    console.error("Stack Trace:", error.stack);
}

Example: Using try-catch Block

Let’s look at a more practical example using try-catch:

function divideNumbers(num1, num2) {
    try {
        if (num2 === 0) {
            throw new Error("Division by zero is not allowed.");
        }
        let result = num1 / num2;
        console.log("Result:", result);
    } catch (error) {
        console.error("Error:", error.message);
    }
}

divideNumbers(10, 0);
divideNumbers(10, 2);
  • Function divideNumbers: This function takes two arguments, num1 and num2.
  • Error Throwing: The function checks if num2 is zero and throws an error if it is.
  • Division: If no error occurs, it divides num1 by num2 and logs the result.
  • Catch Block: If an error is thrown in the try block, the catch block logs the error message.

When divideNumbers(10, 0) is called, the function throws an error because division by zero is not allowed. The error is caught in the catch block, and the message "Division by zero is not allowed." is logged to the console. When divideNumbers(10, 2) is called, the division is successful, and the result "5" is logged.

Optional: The finally Block

The finally block is optional and can be used in conjunction with try-catch. It lets you execute code regardless of whether an error was thrown or not.

What is the finally Block?

The finally block contains code that will run after the try and catch blocks have completed, regardless of whether an error was thrown or not. This is useful for cleanup activities like closing files or releasing resources.

Writing the finally Block

Here’s how you can write a finally block:

try {
    let result = 10 / 0;
} catch (error) {
    console.error("Error:", error.message);
} finally {
    console.log("This will execute regardless of error or not.");
}

Example: Using try-catch-finally Block

Let’s enhance our previous divideNumbers function with a finally block:

function divideNumbers(num1, num2) {
    try {
        if (num2 === 0) {
            throw new Error("Division by zero is not allowed.");
        }
        let result = num1 / num2;
        console.log("Result:", result);
    } catch (error) {
        console.error("Error:", error.message);
    } finally {
        console.log("Finished executing divideNumbers function.");
    }
}

divideNumbers(10, 0);
divideNumbers(10, 2);
  • finally Block: In this example, after the try and catch blocks have executed, the message "Finished executing divideNumbers function." is logged regardless of whether an error was thrown or not.

When divideNumbers(10, 0) is called, an error is thrown and caught, and then the finally block executes. When divideNumbers(10, 2) is called, no error occurs, but the finally block still executes.

Handling Rejected Promises

In JavaScript, asynchronous operations are often handled using Promises. Promises can have three states: pending, fulfilled, and rejected. Errors in Promises need to be handled differently compared to synchronous code.

Introduction to Promises

A Promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Promises are ideal for handling asynchronous code, such as network requests or reading/writing files.

What is a Promise?

A Promise is an object representing the eventual completion or failure of an asynchronous operation. It can be in one of the three states:

  1. Pending: The initial state, neither fulfilled nor rejected.
  2. Fulfilled: Meaning that the operation was completed successfully.
  3. Rejected: Meaning that the operation failed.

States of a Promise: Pending, Fulfilled, Rejected

  • Pending: The promise is in this state when it is neither resolved nor rejected.
  • Fulfilled: This state is reached when the promise is successfully resolved.
  • Rejected: This state is reached when the promise fails to resolve.

Using catch Method

The catch method is used to handle rejected promises. It is a part of the Promise API and is called if a Promise is rejected.

What is the catch Method?

The catch method is a function that gets called when a Promise is rejected. It receives the reason for the rejection, which can be used to handle the error appropriately.

Writing the catch Method

Here’s how you can write a catch method for a Promise:

const fetchData = () => {
    return new Promise((resolve, reject) => {
        // Simulate a network error
        const success = false;
        if (success) {
            resolve("Data fetched successfully");
        } else {
            reject("Failed to fetch data");
        }
    });
};

fetchData()
    .then(result => console.log(result))
   1. catch(error => console.error("Error:", error));
  • catch(error): The catch method takes a function that accepts an error object and can be used to handle errors.

Example: Handling Rejected Promises with catch

Let’s see how the catch method works in action:

const fetchData = () => {
    return new Promise((resolve, reject) => {
        const success = false;
        if (success) {
            resolve("Data fetched successfully");
        } else {
            reject("Failed to fetch data");
        }
    });
};

fetchData()
    .then(result => console.log(result))
    .catch(error => console.error("Error:", error));
  • Simulating a Network Error: In the fetchData function, the success variable is set to false, causing the Promise to be rejected.
  • catch Method: The catch method handles the rejection and logs the error message "Failed to fetch data".

The finally Method for Promises

The finally method for Promises is used to execute code after a Promise has settled, regardless of whether it was fulfilled or rejected.

What is the finally Method?

The finally method allows you to execute code after the Promise has completed. This is useful for cleanup activities, such as closing database connections or freeing up resources.

Writing the finally Method

Here’s how you can write a finally method with a Promise:

const fetchData = () => {
    return new Promise((resolve, reject) => {
        const success = false;
        if (success) {
            resolve("Data fetched successfully");
        } else {
            reject("Failed to fetch data");
        }
    });
};

fetchData()
    .then(result => console.log(result))
    .catch(error => console.error("Error:", error))
    .finally(() => console.log("Execution done."));

Example: Using finally with Promises

Let’s see the finally method in action:

const fetchData = () => {
    return new Promise((resolve, reject) => {
        const success = false;
        if (success) {
            resolve("Data fetched successfully");
        } else {
            reject("Failed to fetch data");
        }
    });
};

fetchData()
    .then(result => console.log(result))
    .catch(error => console.error("Error:", error))
    .finally(() => console.log("Execution done."));
  • finally Method: In this example, "Execution done." will be logged to the console after the then or catch method has executed.

Advanced Error Handling Techniques

Now that we’ve covered the basics of synchronous error handling and handling rejected Promises, let’s dive into some advanced techniques.

Chaining Promises with then and catch

You can chain multiple then methods and a single catch method to handle Promises in a more readable way.

Combining then and catch

When you chain then methods, you can handle each step of an asynchronous operation separately. The catch method can catch errors that occur in any of the previous then methods.

Example: Chained Promises

Let’s see how chaining works:

const fetchData = () => {
    return new Promise((resolve, reject) => {
        const success = true;
        if (success) {
            resolve("Data fetched successfully");
        } else {
            reject("Failed to fetch data");
        }
    });
};

fetchData()
    .then(result => {
        console.log(result);
        return "Data processed successfully";
    })
    .then(result => {
        console.log(result);
    })
    .catch(error => console.error("Error:", error))
    .finally(() => console.log("Execution done."));
  • Chaining Promises: In this example, we fetch data, process it, and then log the results. The catch block will handle any errors that occur in any of the then methods.

Handling Multiple Promises with Promise.all

Promise.all is used to handle multiple Promises concurrently. It returns a single Promise that resolves when all of the input Promises resolve, or rejects if any of the Promises reject.

What is Promise.all?

Promise.all takes an array of Promises and returns a Promise that:

  • Resolves: When all Promises in the array resolve. The resolved value is an array containing the resolved values of the input Promises in the same order.
  • Rejects: When any of the Promises reject. The first rejection reason is passed to the catch method.

Writing Promise.all

Here’s how you can write Promise.all:

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3])
    .then(values => {
        console.log(values);
        // Expected output: Array [3, 42, "foo"]
    })
    .catch(error => console.error("Error:", error));
  • Promise.all: In this example, Promise.all waits for all Promises to resolve and logs the results.

Example: Using Promise.all and Error Handling

Let’s see an example that includes error handling:

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => {
    setTimeout(reject, 100, 'Failed to fetch data');
});
const promise3 = new Promise((resolve, reject) => {
    setTimeout(resolve, 200, 'foo');
});

Promise.all([promise1, promise2, promise3])
    .then(values => {
        console.log(values);
    })
    .catch(error => console.error("Error:", error));
  • Error Handling: In this example, promise2 will reject after 100 milliseconds. The catch method will handle the rejection and log "Error: Failed to fetch data". The then method will not execute because one of the Promises in the array rejected.

Using async and await for Cleaner Code

async and await are syntactic sugar for working with Promises in a more synchronous-like manner. This makes your code easier to read and write.

What is async and await?

An async function is a function declared with the async keyword, and it always returns a Promise. The await keyword can only be used inside an async function and pauses the execution until the Promise is settled.

Writing async Functions

Here’s how you can write an async function:

async function fetchUserData() {
    const response = await fetch("https://api.example.com/user");
    const data = await response.json();
    return data;
}
  • async Function: The fetchUserData function is declared with the async keyword.
  • await Keyword: The await keyword pauses the execution of the function until the Promise returned by fetch resolves.

Handling Errors with try-catch in async Functions

You can use try-catch inside async functions to handle errors more effectively.

Example: Using async and await with Error Handling

Let’s see an example using async and await with error handling:

async function fetchUserData() {
    try {
        const response = await fetch("https://api.example.com/user");
        if (!response.ok) {
            throw new Error("Failed to fetch data");
        }
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.error("Error:", error.message);
    } finally {
        console.log("Finished executing fetchUserData function.");
    }
}

fetchUserData();
  • Error Handling: In this example, if the fetch request fails, a custom error is thrown and caught, and the error message is logged.
  • finally Block: The finally block will execute regardless of whether the request was successful or not.

Practical Example

Let’s build a simple error handling system using the concepts we’ve learned.

Setting Up the Project

Create a new folder for your project and initialize it with a package.json file:

mkdir error-handling-example
cd error-handling-example
npm init -y

Writing Functions with Error Handling

Let’s write a function that fetches user data with error handling:

// fetchUserData.js

async function fetchUserData(userId) {
    try {
        const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
        if (!response.ok) {
            throw new Error("Failed to fetch user data");
        }
        const user = await response.json();
        console.log("User:", user);
        return user;
    } catch (error) {
        console.error("Error:", error.message);
    } finally {
        console.log("Finished fetching user data.");
    }
}

fetchUserData(1);
fetchUserData(1000); // Invalid user ID
  • fetchUserData Function: This function fetches user data from a public API.
  • Error Handling: The function checks if the response is not OK and throws an error if it isn’t.
  • finally Block: The finally block will execute regardless of success or failure.

Handling Rejected Promises in a Real-world Scenario

Let’s expand our example to handle multiple Promises with Promise.all:

// fetchUsers.js

async function fetchUsers() {
    try {
        const user1 = fetchUserData(1);
        const user2 = fetchUserData(1000);

        const users = await Promise.all([user1, user2]);
        console.log("Users:", users);
    } catch (error) {
        console.error("Error fetching users:", error.message);
    } finally {
        console.log("Finished fetching all users.");
    }
}

fetchUsers();
  • fetchUsers Function: This function fetches two users using fetchUserData and handles them using Promise.all.
  • Error Handling: If either user1 or user2 is rejected, the catch block logs the error.
  • finally Block: The finally block will execute regardless of whether the Promises were resolved or rejected.

Conclusion

Review of Key Concepts

In this guide, you’ve learned about:

  • How to handle synchronous errors using try-catch-finally.
  • How to handle asynchronous errors using the catch and finally methods of Promises.
  • Advanced techniques like chaining Promises with then and catch and handling multiple Promises with Promise.all.
  • Using async and await for cleaner asynchronous code and handling errors with try-catch.

Next Steps in Learning JavaScript Error Handling

To further improve your JavaScript skills, consider exploring:

  • Custom Error Classes: Learn how to create custom error classes for better error management.
  • Event Listeners: Use event listeners for more granular control over error handling in events.
  • Global Error Handlers: Implement global error handlers like window.onerror for catching unhandled errors in the browser.

By mastering these techniques, you’ll be able to write more robust and reliable JavaScript applications. Happy coding!


This guide should equip you with a solid understanding of error handling in JavaScript, enabling you to write resilient and maintainable code. Whether you’re working on small scripts or large applications, effective error handling is a crucial skill that will serve you well.