Error Handling Patterns in JavaScript

Improve application stability with proven approaches to managing runtime errors

Posted by Hüseyin Sekmenoğlu on April 18, 2019 Frontend Development

JavaScript is a flexible language but with great flexibility comes responsibility. Errors can occur anywhere in your code due to user input, API calls, async logic or browser limitations. Proper error handling is essential to prevent crashes and improve user experience. In this article you will learn several error handling patterns that will help you build more stable and maintainable JavaScript applications.


⚠️ The Basics: try/catch

The try/catch block is the fundamental tool for catching runtime exceptions.

try {
  const result = riskyOperation();
  console.log(result);
} catch (error) {
  console.error("Something went wrong", error);
}

Use this pattern when working with code that might throw an error immediately, such as parsing JSON or calling functions that throw.


πŸ“₯ Handling Async Errors

Asynchronous functions require special attention. Errors in promises need to be caught using .catch() or try/catch in async functions.

Using .catch()

fetch("/data")
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error("Fetch failed", error));

Using async/await

async function loadData() {
  try {
    const response = await fetch("/data");
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("Error loading data", error);
  }
}

This approach is cleaner and easier to read.


🎯 Fallbacks and Defaults

If your code depends on external data or uncertain conditions, always provide a fallback:

function getUserName(user) {
  return user && user.name ? user.name : "Guest";
}

Or using optional chaining:

const name = user?.name ?? "Guest";

This avoids throwing errors due to undefined or null values.


🧱 Guard Clauses

Guard clauses are used to validate inputs early and exit if something is wrong:

function process(user) {
  if (!user) {
    console.warn("User is required");
    return;
  }

  // continue with processing
}

This prevents deeper errors and keeps your logic safe and readable.


πŸ•ΈοΈ Global Error Handling

Browsers allow you to catch unhandled errors globally:

Synchronous Errors

window.onerror = function(message, source, lineno, colno, error) {
  console.error("Global error:", message);
};

Unhandled Promise Rejections

window.onunhandledrejection = function(event) {
  console.error("Unhandled promise rejection:", event.reason);
};

These handlers can be useful for logging and monitoring.


🧼 Custom Error Classes

Creating your own error types makes it easier to identify and react to specific problems:

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

function validateInput(input) {
  if (input.length < 5) {
    throw new ValidationError("Input too short");
  }
}

Catch custom errors and handle them accordingly:

try {
  validateInput("abc");
} catch (error) {
  if (error instanceof ValidationError) {
    alert(error.message);
  } else {
    throw error;
  }
}

πŸ“‘ Centralized Error Logging

In production apps, errors should be logged for later analysis. You can use tools like:

  • Sentry

  • LogRocket

  • Bugsnag

  • Your custom logging service

Wrap logging inside a generic handler:

function handleError(error) {
  console.error(error);
  // send to server or monitoring tool
}

🚫 What to Avoid

  • Silencing errors without logging

  • Catching all errors and ignoring them

  • Mixing sync and async logic in try/catch carelessly

  • Forgetting to handle promise rejections


βœ… Conclusion

Error handling is not just about avoiding crashes, it is about building resilient systems. By combining try/catch, fallback values, global handlers and logging you can confidently deal with unexpected situations. JavaScript provides all the tools but it is up to you to use them well.