Advanced Error Handling in JavaScript with try, catch, and finally

Hero image for Advanced Error Handling in JavaScript with try, catch, and finally. Image by Maël BALLAND.
Hero image for 'Advanced Error Handling in JavaScript with try, catch, and finally.' Image by Maël BALLAND.

As you will know, error handling is essential for building reliable JavaScript applications, because things inevitably will go wrong. Without proper handling, errors can crash an application/website, or leave users unsure of what's happening. JavaScript provides a structured way to manage errors using the try, catch, and finally statements which allow us to handle problems and maintain a smooth user experience gracefully.

In this article, I'll cover advanced errorhandling techniques in JavaScript, explore how each part works, and discuss best practices to keep our code robust and maintainable.


How try, catch, and finally Work

At their simplest, the try, catch, and finally statements give us control over errors that occur within a block of code:

  • try: contains code that might throw an error.
  • catch: catches and handles errors thrown from the try block.
  • finally: runs code regardless of whether an error occurred.

Example of Basic Error Handling

Here's a fairly straightforward example:

try {  const data = JSON.parse('{invalidJson}');  console.log('Parsed data:', data);} catch (error) {  console.error('Error parsing JSON:', error.message);} finally {  console.log('Cleaning up...');}

If an error occurs in the try block, JavaScript immediately moves to the catch block. Regardless of an error, the finally block runs afterwards, which is useful for tasks like closing resources or resetting states.


Advanced Usage: Custom Error Types

For complex applications, custom error types can help handle errors more effectively.

Creating Custom Errors

By extending JavaScript's builtin Error class, we can create custom error types, like this:

class ValidationError extends Error {  constructor(message: string) {    super(message);    this.name = 'ValidationError';  }}const validateUser = (user: { name?: string }) => {  if (!user.name) {    throw new Error('Missing user name');  }};

Using a custom error:

try {  validateUser({});} catch (error) {  if (error instanceof Error) {    console.log('Validation failed:', error.message);  } else {    throw error;  // rethrow unknown errors  }}

In this way, we can use custom errors to clearly communicate what went wrong, which will help us and our colleagues with code readability and maintainability.


Handling Errors Asynchronously

When working with asynchronous code, handling errors properly becomes even more crucial. JavaScript's async/await syntax integrates seamlessly with try, catch, and finally:

Example of async/await Error Handling

const fetchUserData = async (userId: number) => {  try {    const response = await fetch(`/api/users/${userId}`);    if (!response.ok) {      throw new Error(`Request failed with status ${response.status}`);    }    const user = await response.json();    return user;  } catch (error) {    console.log('Error fetching user data:', error);  } finally {    console.log('Finished user request');  }};

This approach clearly separates successful code paths from the errorhandling logic.


Common Pitfalls in Error Handling

Swallowing Errors

It's really important that, as developers, we avoid ignoring or "swallowing" errors, otherwise, when something does go wrong, we may have no way of working out the root cause. Always handle or explicitly log them, as unhandled errors become difficult to debug:

try {  performRiskyAction();} catch (error) {  // Don't leave this empty!  console.error('An error occurred:', error);}

Overusing try/catch

The flip side of this is, we should use error handling thoughtfully. If we wrap every function call in try and catch blocks, we quickly end up obscuring the real source of errors. Place errorhandling code strategically, especially near code likely to fail or at application boundaries (e.g., network requests).


Testing Error Handling

Reliable applications test error handling thoroughly. Here's how you might test that a function throws an error when expected, using Jest:

test('throws error if user name is missing', () => {  expect(() => validateUser({})).toThrow('Missing user name');});

Including this type of test in your codebase ensures that errors behave predictably and help catch bugs before they reach your users.


Wrapping Up

Advanced error handling using try, catch, and finally helps build JavaScript applications that gracefully manage errors. By thoughtfully handling errors, using custom error types when needed, and testing our errorhandling logic thoroughly, we can deliver robust, maintainable, and userfriendly applications.

Key Takeaways

  • try, catch, and finally provide structured control over errors in JavaScript.
  • Custom errors improve clarity and control for more complex error handling.
  • Always explicitly handle errors to maintain code readability and debug effectively.
  • Async/await integrates naturally with structured error handling.
  • Avoid ignoring errors silently; always log or handle them appropriately.

Understanding and effectively implementing these concepts will help us write robust JavaScript code that handles problems gracefully.


Planning a platform change?

I help teams make difficult platform work clearer, from architecture decisions and migrations to launch recovery, performance, and search visibility.