JavaScript

JavaScript Error Handling: try/catch, Custom Errors, and Best Practices

A complete guide to error handling in JavaScript — from basic try/catch to custom error classes, async error patterns, and production error reporting strategies.

March 14, 20263 min read

The Cost of Silent Failures

The worst bugs aren't errors — they're silent failures. Code that fails without throwing, returns undefined instead of data, or catches errors and does nothing.

// TERRIBLE: error is swallowed, data is undefined, page shows blank

try {

const data = await fetch('/api/data').then(r => r.json());

setState(data);

} catch (e) {

// empty catch — the worst anti-pattern

}

// BETTER: log, show user-facing error, report to monitoring

try {

const data = await fetch('/api/data').then(r => r.json());

setState(data);

} catch (error) {

console.error('Data fetch failed:', error);

setError('Unable to load data. Please try again.');

reportError(error); // Send to Sentry/LogRocket/etc.

}

Every production app should follow this rule: if you catch it, handle it. If you can't handle it, let it propagate.

Custom Error Classes

Built-in Error gives you a message and stack trace but no context. Create custom errors:

class AppError extends Error {

constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {

super(message);

this.name = 'AppError';

this.statusCode = statusCode;

this.code = code;

}

}

class ValidationError extends AppError {

constructor(message, fields = {}) {

super(message, 400, 'VALIDATION_ERROR');

this.name = 'ValidationError';

this.fields = fields;

}

}

class NotFoundError extends AppError {

constructor(resource = 'Resource') {

super(${resource} not found, 404, 'NOT_FOUND');

this.name = 'NotFoundError';

}

}

// Usage

function getUser(id) {

const user = users.find(u => u.id === id);

if (!user) throw new NotFoundError('User');

return user;

}

function createUser(data) {

const errors = {};

if (!data.email) errors.email = 'Email is required';

if (!data.name) errors.name = 'Name is required';

if (Object.keys(errors).length > 0) {

throw new ValidationError('Invalid input', errors);

}

}

Custom errors let your error handler respond appropriately based on the error type.

Global Error Handling

Set up a safety net for unhandled errors:

Browser:
// Catch unhandled errors

window.addEventListener('error', (event) => {

console.error('Unhandled error:', event.error);

reportError(event.error);

});

// Catch unhandled Promise rejections

window.addEventListener('unhandledrejection', (event) => {

console.error('Unhandled rejection:', event.reason);

reportError(event.reason);

event.preventDefault(); // Prevent default console error

});

Node.js:
process.on('uncaughtException', (error) => {

console.error('Uncaught exception:', error);

reportError(error);

process.exit(1); // Exit — the process is in an unknown state

});

process.on('unhandledRejection', (reason) => {

console.error('Unhandled rejection:', reason);

reportError(reason);

});

These are safety nets, not primary error handling. Every async operation should have its own error handling.

Error Boundaries in React

React error boundaries catch rendering errors that try/catch can't:

"use client";

import { Component, type ReactNode } from 'react';

interface Props {

children: ReactNode;

fallback?: ReactNode;

}

interface State {

hasError: boolean;

error: Error | null;

}

class ErrorBoundary extends Component {

state: State = { hasError: false, error: null };

static getDerivedStateFromError(error: Error) {

return { hasError: true, error };

}

componentDidCatch(error: Error, info: React.ErrorInfo) {

console.error('Component error:', error, info.componentStack);

}

render() {

if (this.state.hasError) {

return this.props.fallback || (

Something went wrong

);

}

return this.props.children;

}

}

Wrap sections of your app that might fail:

Failed to load widget

}>

In Next.js App Router, you can also use error.tsx files for route-level error handling.

Production Error Reporting

In production, errors need to go somewhere you can see them:

function reportError(error, context = {}) {

// 1. Always log locally

console.error('[Error]', error.message, context);

// 2. Send to your error tracking service

if (typeof window !== 'undefined') {

// Browser: send to backend

navigator.sendBeacon('/api/errors', JSON.stringify({

message: error.message,

stack: error.stack,

url: window.location.href,

timestamp: new Date().toISOString(),

userAgent: navigator.userAgent,

...context,

}));

}

}

navigator.sendBeacon is ideal for error reporting because it sends data even if the page is closing — unlike fetch which gets cancelled on navigation.

For production apps, use a dedicated service like Sentry, LogRocket, or Betterstack. They provide stack traces, user session replay, error grouping, and alerting.

javascripterror handlingdebuggingbest practicesproductiontutorial

Related Articles