JavaScript Error Handling Best Practices

Errors are inevitable in any real-world application. Network requests fail, users input unexpected data, and third-party APIs go down. The difference between a robust application and a fragile one is not the absence of errors — it's how gracefully those errors are handled. Here's a practical guide to error handling in JavaScript.

1. Use try/catch Correctly

The basic building block is the try/catch block. Use it around code that may throw:

try {
  const data = JSON.parse(userInput); // May throw SyntaxError
  processData(data);
} catch (error) {
  console.error("Failed to parse input:", error.message);
  // Show a user-friendly message
}

Tip: Don't catch errors silently. At minimum, log them. Silent catch blocks hide bugs that become nightmares to debug later.

2. Differentiate Error Types

JavaScript has built-in error types you can check against:

try {
  riskyOperation();
} catch (error) {
  if (error instanceof TypeError) {
    console.warn("Type issue:", error.message);
  } else if (error instanceof RangeError) {
    console.warn("Range issue:", error.message);
  } else {
    throw error; // Re-throw errors you don't know how to handle
  }
}

Re-throwing is important — don't swallow errors that are outside your expected failure modes.

3. Create Custom Error Classes

Custom error types make your error handling much more expressive and testable:

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

function validateAge(age) {
  if (typeof age !== "number") {
    throw new ValidationError("Age must be a number", "age");
  }
  if (age < 0 || age > 120) {
    throw new ValidationError("Age out of valid range", "age");
  }
}

try {
  validateAge("old");
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Field "${error.field}": ${error.message}`);
  }
}

4. Handle Async Errors Properly

With async/await, use try/catch just like synchronous code:

async function fetchData(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error("Fetch failed:", error.message);
    return null; // Return a safe fallback
  }
}

With Promises, always attach a .catch() handler. Unhandled promise rejections can crash Node.js applications and cause silent failures in browsers.

5. Use a Global Error Handler

Catch unexpected errors that slip through your local handlers:

// Browser
window.addEventListener("error", (event) => {
  console.error("Unhandled error:", event.error);
});

window.addEventListener("unhandledrejection", (event) => {
  console.error("Unhandled promise rejection:", event.reason);
});

// Node.js
process.on("uncaughtException", (error) => {
  console.error("Uncaught exception:", error);
  process.exit(1); // Exit cleanly after logging
});

6. Provide User-Friendly Feedback

Technical error messages belong in logs, not in UI. Map your errors to human-readable messages:

function getUserMessage(error) {
  if (error instanceof ValidationError) return `Please check: ${error.field}`;
  if (error.message.includes("NetworkError")) return "Check your internet connection.";
  return "Something went wrong. Please try again.";
}

Quick Checklist

  • ✅ Never silently swallow errors in empty catch blocks.
  • ✅ Re-throw errors you can't handle.
  • ✅ Use custom error classes for domain-specific failures.
  • ✅ Always handle async errors (await + try/catch or .catch()).
  • ✅ Separate error messages for logs vs. user-facing UI.
  • ✅ Set up global error handlers as a safety net.

Conclusion

Good error handling is a mark of a professional developer. It makes your applications more resilient, your debugging sessions shorter, and your users' experiences far less frustrating. Start applying these patterns today and your future self will thank you.