DEV Community

Alex Chen
Alex Chen

Posted on

Error Handling in JavaScript: Beyond try/catch

Error Handling in JavaScript: Beyond try/catch

Most developers only scratch the surface. Here's the full picture.

The Basics (Quick Refresher)

try {
  // Code that might throw
  riskyOperation();
} catch (error) {
  // Handle the error
  console.error('Something went wrong:', error.message);
} finally {
  // Always runs (cleanup, close connections)
  cleanup();
}
Enter fullscreen mode Exit fullscreen mode

Custom Error Classes

// Base application error
class AppError extends Error {
  constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    this.code = code;        // Machine-readable code for API responses
    this.timestamp = new Date().toISOString();
    Error.captureStackTrace(this, this.constructor);
  }
}

// Specific error types
class NotFoundError extends AppError {
  constructor(resource, id) {
    super(`${resource} with id '${id}' not found`, 404, 'NOT_FOUND');
    this.resource = resource;
    this.id = id;
  }
}

class ValidationError extends AppError {
  constructor(fields) {
    super('Validation failed', 422, 'VALIDATION_ERROR');
    this.fields = fields; // { email: 'Invalid format', name: 'Required' }
  }

  toJSON() {
    return {
      error: { ...this, fields: this.fields },
    };
  }
}

class RateLimitError extends AppError {
  constructor(retryAfter) {
    super('Too many requests', 429, 'RATE_LIMITED');
    this.retryAfter = retryAfter;
  }
}

// Usage:
function getUser(id) {
  const user = database.find(id);
  if (!user) throw new NotFoundError('User', id);
  return user;
}
Enter fullscreen mode Exit fullscreen mode

Async Error Handling Patterns

// Pattern 1: try/catch in async function
async function fetchUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) throw new NotFoundError('User', id);
    return await response.json();
  } catch (error) {
    if (error instanceof NotFoundError) {
      showNotFoundPage();
      return null;
    }
    // Re-throw unexpected errors to be handled higher up
    throw error;
  }
}

// Pattern 2: Wrapper that returns [data, error] tuple
async function tryCatch(promise) {
  try {
    const data = await promise;
    return [data, null];
  } catch (error) {
    return [null, error];
  }
}

// Usage — no try/catch needed!
const [user, error] = await tryCatch(fetchUser(123));
if (error) {
  handleError(error);
  return;
}
console.log(user.name);

// Pattern 3: Higher-order function wrapper
const withErrorHandler = (fn) => async (...args) => {
  try {
    return await fn(...args);
  } catch (error) {
    logError(error);
    return formatErrorResponse(error);
  }
};

app.get('/api/users/:id', withErrorHandler(async (req, res) => {
  const user = await fetchUser(req.params.id);
  res.json(user); // If fetchUser throws, withErrorHandler catches it
}));
Enter fullscreen mode Exit fullscreen mode

Global Error Handling

// Node.js uncaught exceptions
process.on('uncaughtException', (error) => {
  console.error('UNCAUGHT EXCEPTION:', error);
  // Log to external service
  logToService(error);

  // Don't just crash! But also don't keep running blindly.
  // Best practice: Graceful shutdown
  gracefulShutdown();

  // Note: After uncaughtException, your app state is uncertain.
  // Restart is usually the safest option.
});

// Unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
  console.error('UNHANDLED REJECTION:', reason);
  logToService({ type: 'unhandledRejection', reason });
});

// Browser global handler
window.onerror = function(message, source, lineno, colno, error) {
  console.error('Global error:', message, source, lineno);
  reportToErrorTrackingService({ message, source, lineno, colno, stack: error?.stack });
};

window.addEventListener('unhandledrejection', (event) => {
  event.preventDefault(); // Prevent default logging
  reportToErrorTrackingService({ reason: String(event.reason) });
});
Enter fullscreen mode Exit fullscreen mode

Express.js Error Handling

// 404 handler (must be after all routes)
app.use((req, res, _next) => {
  res.status(404).json({
    error: { code: 'NOT_FOUND', message: `${req.method} ${req.path} not found` },
  });
});

// Centralized error handler (MUST have 4 parameters!)
app.use((err, req, res, _next) => {
  console.error(err.stack);

  // Known app errors → send structured response
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: { code: err.code, message: err.message },
    });
  }

  // JSON parse error
  if (err.type === 'entity.parse.failed') {
    return res.status(400).json({
      error: { code: 'INVALID_JSON', message: 'Malformed JSON in request body' },
    });
  }

  // Unknown errors → generic message in production
  const statusCode = err.statusCode || 500;
  const message = process.env.NODE_ENV === 'production'
    ? 'Internal server error'
    : err.message;

  res.status(statusCode).json({
    error: { code: 'INTERNAL_ERROR', message },
    ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
  });
});
Enter fullscreen mode Exit fullscreen mode

Error Reporting & Monitoring

// Simple error reporting service
class ErrorReporter {
  constructor(serviceUrl, apiKey) {
    this.serviceUrl = serviceUrl;
    this.apiKey = apiKey;
    this.queue = [];
    this.flushInterval = setInterval(() => this.flush(), 10000);
  }

  async report(error, context = {}) {
    const entry = {
      message: error.message || String(error),
      stack: error.stack,
      context: {
        url: typeof window !== 'undefined' ? window.location.href : undefined,
        userId: context.userId,
        route: context.route,
        timestamp: new Date().toISOString(),
        environment: process.env.NODE_ENV,
        ...context,
      },
    };

    this.queue.push(entry);

    if (this.queue.length >= 10) {
      this.flush(); // Flush immediately if queue is large
    }
  }

  async flush() {
    if (this.queue.length === 0) return;

    const entries = [...this.queue];
    this.queue = [];

    try {
      await fetch(this.serviceUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiKey },
        body: JSON.stringify(entries),
      });
    } catch (e) {
      // Can't report errors? Put them back
      this.queue.unshift(...entries);
    }
  }
}

// Usage:
const reporter = new ErrorReporter('https://errors.example.com/api/report', 'api-key-123');

try {
  riskyOperation();
} catch (error) {
  await reporter.report(error, { userId: user.id, route: '/dashboard' });
  // Show user-friendly message
  showToast('Something went wrong. We\'ve been notified.');
}
Enter fullscreen mode Exit fullscreen mode

Error Boundaries (React)

// React class component for catching rendering errors
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, info) {
    // Log to error service
    logErrorToService(error, { componentStack: info.componentStack });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h2>Something went wrong</h2>
          <button onClick={() => this.setState({ hasError: false })}>
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Wrap your app or specific components:
<ErrorBoundary>
  <Dashboard />
</ErrorBoundary>
Enter fullscreen mode Exit fullscreen mode

What's the trickiest error handling scenario you've faced?

Follow @armorbreak for more JavaScript content.

Top comments (0)