← Back to DevBytes

Error Handling Patterns in JavaScript

What is Error Handling?

Error handling is the practice of anticipating, detecting, and gracefully managing runtime errors in your JavaScript applications. Instead of letting an unexpected exception crash the entire program or leave the user staring at a frozen screen, error handling provides a structured way to capture failure states, log diagnostic information, and decide on fallback behavior.

Why It Matters

Without proper error handling, a single thrown exception can break your entire application, corrupt shared state, or leak sensitive information through stack traces in production. In server-side Node.js applications, uncaught exceptions may crash the process, causing downtime. In browser-based apps, an unhandled error can stop further JavaScript execution, breaking interactivity. Robust error handling patterns ensure:

Core Error Handling Mechanisms

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Try…Catch…Finally

The fundamental block for synchronous error handling. Wrap code that may throw in try, handle the error in catch, and execute cleanup logic in finally.

function parseJson(input) {
  try {
    const result = JSON.parse(input);
    console.log('Parsed successfully:', result);
    return result;
  } catch (error) {
    console.error('Parse failed:', error.message);
    // Provide a fallback or rethrow
    return null;
  } finally {
    console.log('Cleanup: releasing resources if needed');
  }
}

parseJson('{ "valid": true }');  // works
parseJson('invalid string');     // caught, returns null

Throwing Custom Errors

Throwing your own errors allows you to attach domain-specific information and differentiate error types. Use the throw keyword with built-in error constructors (Error, TypeError, RangeError) or custom classes.

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
    this.code = 400;
  }
}

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

try {
  validateEmail('user-at-example');
} catch (err) {
  if (err instanceof ValidationError) {
    console.warn(`Validation failed on ${err.field}: ${err.message}`);
  } else {
    console.error('Unexpected error:', err);
  }
}

Custom error classes enable selective catching based on instanceof checks, keeping error handling precise.

Asynchronous Error Handling Patterns

Promise Chains with .catch()

With Promise-based APIs, errors are caught using the .catch() method. If any .then() callback throws or a promise rejects, control flows to the nearest .catch().

fetch('/api/data')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    return response.json();
  })
  .then(data => {
    console.log('Data:', data);
  })
  .catch(error => {
    console.error('Fetch failed:', error.message);
    // Optional: display user-friendly message
  })
  .finally(() => {
    console.log('Request finished.');
  });

Async/Await with Try…Catch

async/await makes asynchronous code look synchronous, and you can wrap it with traditional try/catch. This improves readability and allows handling errors at the call site.

async function loadUserProfile(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) throw new Error(`Status ${response.status}`);
    const profile = await response.json();
    return profile;
  } catch (error) {
    console.error('Failed to load profile:', error);
    // Decide whether to rethrow or return a default
    return { name: 'Guest', email: null };
  } finally {
    console.log('Profile fetch attempt completed');
  }
}

// Usage
loadUserProfile(42).then(profile => console.log(profile));

Handling Multiple Async Operations

When dealing with several independent asynchronous tasks, you can use Promise.allSettled() to avoid a single rejection aborting the entire group.

const promises = [
  fetch('/api/orders').then(r => r.json()),
  fetch('/api/inventory').then(r => r.json()),
  Promise.reject(new Error('Simulated failure'))
];

const results = await Promise.allSettled(promises);
results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    console.log(`Promise ${index} succeeded:`, result.value);
  } else {
    console.error(`Promise ${index} failed:`, result.reason);
  }
});

Error Handling in Event Listeners and Timers

DOM Event Handlers

Errors thrown inside event handlers do not propagate to an outer try/catch because event dispatch happens later. Wrap handler bodies individually.

document.querySelector('#submit-btn').addEventListener('click', () => {
  try {
    // Complex logic that may throw
    const input = document.querySelector('#username').value;
    if (!input) throw new Error('Username required');
    submitForm(input);
  } catch (error) {
    console.error('Submit handler failed:', error);
    // Show inline error message instead of breaking the page
  }
});

setTimeout / setInterval

Similarly, callbacks scheduled with setTimeout or setInterval run outside the current execution context, so wrap them internally.

setTimeout(() => {
  try {
    riskyOperation();
  } catch (e) {
    console.error('Scheduled operation failed:', e);
  }
}, 1000);

Global Error Handlers

For errors that slip through local handling, JavaScript provides global hooks. These are essential as a last-resort safety net, especially in production.

Browser: window.onerror & unhandledrejection

window.onerror = function(message, source, lineno, colno, error) {
  console.error('Global catch:', message, 'at', source, lineno);
  // Optionally send to analytics service
  return true; // Prevent default browser logging
};

window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled promise rejection:', event.reason);
  // Prevent default (some browsers show an error overlay)
  event.preventDefault();
});

Node.js: process Events

process.on('uncaughtException', (error) => {
  console.error('Fatal uncaught exception:', error);
  // Perform cleanup then exit
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection at:', promise, 'reason:', reason);
  // Application can continue, but it's a sign of a bug
});

Advanced Error Handling Patterns

Error Boundary Pattern (Functional)

Inspired by React Error Boundaries, you can create higher-order functions that wrap any function and catch errors, returning a fallback value or logging.

function withErrorBoundary(fn, fallbackValue) {
  return function(...args) {
    try {
      return fn.apply(this, args);
    } catch (error) {
      console.error(`Error in ${fn.name || 'anonymous'}:`, error);
      return fallbackValue;
    }
  };
}

const riskyCalculation = withErrorBoundary(
  (input) => complexMath(input),
  null
);

const result = riskyCalculation(5); // returns null on error instead of crashing

Result / Either Pattern

Instead of throwing, some functions return a discriminated union (often called "Result") that explicitly indicates success or failure. This forces the caller to handle both cases.

function safeParseJson(input) {
  try {
    const data = JSON.parse(input);
    return { ok: true, value: data };
  } catch (error) {
    return { ok: false, error: error.message };
  }
}

const result = safeParseJson(userInput);
if (result.ok) {
  console.log('Parsed:', result.value);
} else {
  console.error('Parse error:', result.error);
}

This pattern eliminates hidden exceptions and makes error handling explicit and predictable.

Centralized Error Handler in Express.js

In Node.js web applications, a centralized error-handling middleware catches errors from route handlers and sends standardized responses.

// Route handler with async error wrapper
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/data', asyncHandler(async (req, res) => {
  const data = await fetchExternalData();
  res.json(data);
}));

// Centralized error middleware (must have 4 parameters)
app.use((err, req, res, next) => {
  console.error('Request error:', err.stack);
  const status = err.status || 500;
  res.status(status).json({
    error: {
      message: err.message,
      status
    }
  });
});

Best Practices

Conclusion

Error handling in JavaScript is not just about preventing crashes — it’s a fundamental design discipline that shapes the robustness, maintainability, and user experience of your applications. By combining synchronous try/catch blocks, custom error classes, asynchronous patterns like .catch() and async-aware wrappers, global fallback handlers, and explicit result objects, you create a layered defense against runtime failures. Adopting these patterns consistently across your codebase will make your applications more predictable, easier to debug, and resilient in the face of unexpected conditions. Remember, every unhandled error is a bug waiting to surface; every handled error is an opportunity for graceful recovery.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles