DEV Community

Omri Luz
Omri Luz

Posted on

22 2 3 2 2

Error Propagation with Custom Error Types

Error Propagation with Custom Error Types: The Definitive Guide

Introduction

Error handling is a fundamental aspect of programming that directly affects user experience, application stability, and maintainability. In JavaScript, error handling has evolved considerably since the inception of the language. With the introduction of custom error types, developers have the ability to propagate errors more meaningfully, providing greater insight into the issues their applications encounter.

Historical Context

JavaScript error handling dates back to the language's inception in the mid-1990s. Initially, errors were represented by a single Error object. However, as applications grew in complexity, developers began to grapple with the limitations of this approach. The lack of specificity meant that all errors were treated equally, making debugging arduous.

With the advent of ES6 (introduced in 2015), JavaScript introduced the ability to create custom error types. With the rise of JavaScript frameworks, libraries, and the asynchronous programming paradigm (e.g., Promises and Async/Await), custom error types have become indispensable for building robust web applications.

Custom Error Types

Defining Custom Errors

A custom error type allows you to create meaningful error brands in your JavaScript applications. The foundation of creating a custom error type is extending the built-in Error class.

Basic Custom Error Implementation

Here’s a simple implementation of a custom error type:

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError'; // Custom error name
  }
}
Enter fullscreen mode Exit fullscreen mode

Throwing and Catching Custom Errors

A significant aspect of error propagation is throwing and catching these custom errors during application execution. Here’s an example function that might validate user input and throw a ValidationError appropriately.

function validateInput(input) {
  if (!input) {
    throw new ValidationError('Input cannot be empty.');
  }
  return true;
}

try {
  validateInput('');
} catch (error) {
  if (error instanceof ValidationError) {
    console.error(`Validation error occurred: ${error.message}`);
  } else {
    console.error('An unexpected error occurred:', error);
  }
}
Enter fullscreen mode Exit fullscreen mode

Complex Scenarios

As applications grow, error propagation can become convoluted, especially in asynchronous code. Here’s an example demonstrating errors in asynchronous functions using Promises.

Asynchronous Error Propagation

Consider a scenario where you fetch data from an API, and the data must meet specific validation criteria.

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

async function fetchData(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new FetchError(`Failed to fetch data: ${response.statusText}`);
  }

  const data = await response.json();
  validateData(data);
  return data;
}

function validateData(data) {
  if (!data.valid) {
    throw new ValidationError('Data is not valid.');
  }
}

fetchData('https://api.example.com/data')
  .then(data => console.log('Fetched data:', data))
  .catch(error => {
    if (error instanceof FetchError) {
      console.error(`Fetch error: ${error.message}`);
    } else if (error instanceof ValidationError) {
      console.error(`Validation error: ${error.message}`);
    } else {
      console.error('Unknown error occurred:', error);
    }
  });
Enter fullscreen mode Exit fullscreen mode

In this example, both fetch and validation errors are captured separately, enabling appropriate handling of each error type.

Advanced Implementations

Error Propagation in Call Chains

In larger applications, multiple layers might handle errors. Propagating errors through call chains becomes essential. Here’s an implementation that shows how an error can be propagated through several functions.

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

function simulateDatabaseOperation() {
  throw new DatabaseError('Database connection failed');
}

function performDatabaseOperation() {
  return simulateDatabaseOperation();
}

function applicationLayer() {
  try {
    performDatabaseOperation();
  } catch (error) {
    if (error instanceof DatabaseError) {
      console.error(`Database error caught: ${error.message}`);
      throw new Error('Higher-level error handling triggered.');
    }
    throw error; // Rethrow non-database errors
  }
}

try {
  applicationLayer();
} catch (error) {
  console.error('Caught in main application:', error.message);
}
Enter fullscreen mode Exit fullscreen mode

Handling Multiple Errors Concurrently

With the introduction of Promise.all(), applications often need to handle multiple asynchronous operations concurrently. Suppose two separate validations need to occur:

async function validateUserInput(input) {
  if (input.username.length < 3) {
    throw new ValidationError('Username too short.');
  }
}

async function validateUserAge(input) {
  if (input.age < 18) {
    throw new ValidationError('Must be at least 18 years old.');
  }
}

async function validateUserData(input) {
  return Promise.all([
    validateUserInput(input),
    validateUserAge(input)
  ]).catch(error => {
    if (error instanceof ValidationError) {
      console.error(`Validation error: ${error.message}`);
    }
    throw error; // Further propagation
  });
}

validateUserData({ username: 'ab', age: 19 })
  .catch(error => console.error('Error during user data validation:', error.message));
Enter fullscreen mode Exit fullscreen mode

Comparative Analysis with Other Approaches

Standard Error Handling vs. Custom Types

Using standard error handling (i.e., without custom types) leads to ambiguous error management. Custom types allow for:

  • Clarity: Understanding the type of error without inspecting the message.
  • Specific handling: Catching specific errors based on their type.
  • Extensibility: Introducing new error types for specific domains or modules has a low overhead.

Alternative Libraries

  1. express-async-errors: Automates error handling in Express applications.
  2. http-errors: Simplifies creating HTTP errors with proper status codes.

While these libraries abstract some complexities, they shift the responsibility of custom error types away from the developer.

Real-World Use Cases

  • Microservices Architecture: Each microservice can define and throw tailored errors that other services can consume. For example, an inventory service might throw InventoryError if a product isn't available.

  • API Development: RESTful APIs benefit from well-defined error types, making it clear to the consumers of the API what went wrong (e.g., UnauthorizedError, NotFoundError).

Performance Considerations

Memory Overhead

Creating numerous custom error types can lead to increased memory consumption. Each instantiation comes with its own stack trace – ensure you are only creating them when necessary.

Error Handling Overhead

Frequent error checks slow down the application, especially in tight loops or hot paths. Limit the frequency and ensure critical paths are optimized.

Advanced Debugging Techniques

  • Stack Traces: Utilize the full capability of error stack traces to trace back to the origin of complex error types.

  • Logging Libraries: Implement libraries like winston or bunyan for structured logging of errors which can help in capturing detailed context about errors in production.

  • Error Monitoring Services: Use services like Sentry or Rollbar for real-time error tracking, aggregation, and monitoring.

Conclusion

Custom error types enhance error propagation in JavaScript applications, leading to improved clarity, maintainability, and user experience. Through the use of custom types, developers can create robust systems that handle a variety of errors gracefully.

By understanding the nuances of error propagation, considering practical implementations, and being aware of potential pitfalls, senior developers can elevate their error handling strategies, making their applications more resilient in the face of adverse conditions.

References

This article aims to be a comprehensive resource for senior developers looking to leverage custom error types for better error handling in their JavaScript applications. The strategies discussed here are not just theoretical; they reflect practical challenges and solutions encountered in the field, making this an essential guide for building resilient software in JavaScript.

Top comments (3)

Collapse
 
hilleer profile image
Daniel Hillmann • Edited

You could also have a single application error that includes properties to know how to handle it. With larger applications you will suddenly have an overwhelming amount of error kinds.

class ApplicationError extends Error {
  // define application error props
  constructor(myProps) {
    // Set props accordingly
  }
}
Enter fullscreen mode Exit fullscreen mode

Then in places like an API error handler you need to check for a single error kind and simply extract the details needed.

Collapse
 
roman_shestakov_725410440 profile image
Roman Shestakov • Edited

Not to forget, custom methods could be added to the new Error class

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

  toString() {
    return `${this.name}: ${this.message}`;
  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
evanskaufman profile image
Evan K

This is great, thank you for the extensive writeup!

Also, don't sleep on the (now widely available) error.cause property, which I've found very useful in nested error handling

Image of PulumiUP 2025

Let's talk about the current state of cloud and IaC, platform engineering, and security.

Dive into the stories and experiences of innovators and experts, from Startup Founders to Industry Leaders at PulumiUP 2025.

Register Now

👋 Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay