JavaScript Error Handling Patterns

Hero image for JavaScript Error Handling Patterns. Image by Etienne Girardet.
Hero image for 'JavaScript Error Handling Patterns.' Image by Etienne Girardet.

In JavaScript development, errors are an inevitable part of the process. Whether they stem from unexpected user input, network outages, or programming oversights, managing these errors effectively is vital in building reliable, maintainable, and userfriendly applications. In this article, I intend to dive into modern errorhandling practices and patterns which can help us craft resilient JavaScript applications.


Why Modern Error Handling Matters

Modern JavaScript applications are increasingly complex, often relying on asynchronous operations, external APIs, and dynamic user interactions. Without proper error handling, these complexities can lead to:

  • Unreliable applications:

    Errors can cause unexpected crashes or data loss.
  • Poor user experience:

    Users may encounter confusing or frustrating behaviour.
  • Debugging difficulties:

    Uncaught or poorly managed errors make diagnosing issues harder.

Effective errorhandling strategies mitigate these problems, ensuring stability and an improved developer experience.


Key Patterns and Practices for Error Handling

1. try-catch for Synchronous Errors

The try-catch construct is a foundational errorhandling tool for managing synchronous operations. It isolates potentially errorprone code, allowing the program to respond gracefully when an error does occur.

For example:

try {  const data = JSON.parse('{"key": "value"}');  console.log(data.key);  // value} catch (error) {  console.error('Failed to parse JSON:', error.message);}

Use case: Handling errors in operations like JSON parsing, DOM manipulation, or synchronous computations.

2. Handling Errors in Promises

Promises represent asynchronous operations, and error handling is central to their design. The .catch() method captures errors that occur within a promise chain.

fetch('https://api.example.com/data')  .then((response) => {    if (!response.ok) {      throw new Error('Network response was not ok');    }    return response.json();  })  .then((data) => console.log(data))  .catch((error) => console.error('Fetch failed:', error));

Best practice: Always include a .catch() block in your promise chains to avoid unhandled rejections.

3. Using async/await with try-catch

The async/await syntax simplifies working with asynchronous code, but it's essential to wrap these operations in try-catch blocks to handle errors too.

async function fetchData() {  try {    const response = await fetch('https://api.example.com/data');    if (!response.ok) {      throw new Error('Network error');    }    const data = await response.json();    console.log(data);  } catch (error) {    console.error('Error fetching data:', error.message);  }}

Advantage: Provides a more readable alternative to promise chains, making error handling clearer.

4. Global Error Handling

Errors that escape local handling can be caught globally using event listeners for window.onerror (for synchronous errors) and unhandledrejection (for unhandled promise rejections).

For example:

window.addEventListener('error', (event) => {  console.error('Global error:', event.message);});window.addEventListener('unhandledrejection', (event) => {  console.error('Unhandled rejection:', event.reason);});

Caution: Use global handlers as a safety net, not a replacement for local error handling.

5. Custom Error Classes

For more descriptive error messages, custom error classes allow us to create errors tailored to specific contexts:

class ApiError extends Error {  constructor(message, statusCode) {    super(message);    this.name = 'ApiError';    this.statusCode = statusCode;  }}try {  throw new ApiError('Resource not found', 404);} catch (error) {  console.error(    `${error.name}: ${error.message} (Status: ${error.statusCode})`  );}

Benefit: Enables differentiation between error types and provides contextual information for debugging.

6. Logging and Monitoring

Error logging tools like Sentry or LogRocket help track errors in production environments, providing detailed reports and diagnostics. Of course, it is then important to avoid logging sensitive user data in order to maintain security and compliance. But, you knew that already anyway, right?


Wrapping Up

Modern JavaScript error handling has evolved to address the growing complexity of web applications. From try-catch for synchronous errors to production monitoring tools, these patterns ensure that applications remain reliable and userfriendly, and that we as developers can keep on top of any issues that might arise.

Key Takeaways

  • Foundational tools:

    Use try-catch for synchronous errors and .catch() for promises.
  • Asynchronous handling:

    Wrap async/await operations in try-catch.
  • Global safety nets:

    Implement global error handlers for uncaught errors.
  • Custom errors:

    Create error classes for specific scenarios to improve clarity.
  • Monitoring:

    Use logging and monitoring tools like Sentry to track errors in production.

Used carefully, these patterns make failures easier to isolate, report, and recover from before they become repeated userfacing bugs.


Have a complex web platform issue?

Tell me what is blocked, what has changed, and what needs to be true after the fix. I'll come back with a practical next step.