JavaScript Error Fundamentals

Built-in Error Types

// Error - base error type
throw new Error('Something went wrong');

// TypeError - wrong type
const obj = null;
obj.method(); // TypeError: Cannot read property 'method' of null

// ReferenceError - undefined variable
console.log(undefinedVar); // ReferenceError

// SyntaxError - invalid syntax (usually at parse time)
JSON.parse('invalid json'); // SyntaxError

// RangeError - value out of range
new Array(-1); // RangeError: Invalid array length

// Custom errors
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
  }
}

throw new ValidationError('Email is required', 'email');

Try-Catch-Finally

// Basic try-catch
try {
  const data = JSON.parse(invalidJson);
} catch (error) {
  console.error('Parse error:', error.message);
}

// With finally (always runs)
try {
  await database.connect();
  await database.query('SELECT * FROM users');
} catch (error) {
  console.error('Database error:', error);
} finally {
  await database.disconnect(); // Always runs
}

// Catching specific errors
try {
  await someOperation();
} catch (error) {
  if (error instanceof ValidationError) {
    // Handle validation error
    console.log('Validation failed:', error.field);
  } else if (error instanceof NetworkError) {
    // Handle network error
    console.log('Network failed, retrying...');
  } else {
    // Re-throw unknown errors
    throw error;
  }
}

// Nested try-catch
try {
  try {
    riskyOperation();
  } catch (error) {
    // Handle or transform error
    throw new Error(`Operation failed: ${error.message}`);
  }
} catch (error) {
  // Handle transformed error
  console.error(error.message);
}

Custom Error Classes

// Base application error
class AppError extends Error {
  constructor(message, statusCode = 500, isOperational = true) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    this.isOperational = isOperational;
    this.timestamp = new Date().toISOString();

    Error.captureStackTrace(this, this.constructor);
  }
}

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

class ValidationError extends AppError {
  constructor(message, errors = []) {
    super(message, 400);
    this.errors = errors;
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(message, 401);
  }
}

class ForbiddenError extends AppError {
  constructor(message = 'Access denied') {
    super(message, 403);
  }
}

class ConflictError extends AppError {
  constructor(message = 'Resource already exists') {
    super(message, 409);
  }
}

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

// Usage
if (!user) {
  throw new NotFoundError('User');
}

if (!isValid) {
  throw new ValidationError('Invalid input', [
    { field: 'email', message: 'Invalid email format' },
    { field: 'password', message: 'Password too short' },
  ]);
}

Express.js Error Handling

Error Handling Middleware

// middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
  // Log error
  console.error({
    message: err.message,
    stack: err.stack,
    timestamp: new Date().toISOString(),
    path: req.path,
    method: req.method,
    ip: req.ip,
  });

  // Operational errors (expected)
  if (err.isOperational) {
    return res.status(err.statusCode).json({
      success: false,
      error: {
        message: err.message,
        ...(err.errors && { errors: err.errors }),
      },
    });
  }

  // Programming/unknown errors
  // Don't leak error details in production
  const message = process.env.NODE_ENV === 'production'
    ? 'Internal server error'
    : err.message;

  res.status(500).json({
    success: false,
    error: { message },
  });
};

// Async error wrapper (eliminates try-catch in routes)
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Usage in routes
app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) {
    throw new NotFoundError('User');
  }
  res.json(user);
}));

// 404 handler (before error handler)
app.use((req, res, next) => {
  next(new NotFoundError('Endpoint'));
});

// Error handler (must be last)
app.use(errorHandler);

Handling Specific Errors

// Handle different error types
const errorHandler = (err, req, res, next) => {
  let error = { ...err };
  error.message = err.message;

  // Mongoose bad ObjectId
  if (err.name === 'CastError') {
    error = new AppError('Invalid ID format', 400);
  }

  // Mongoose duplicate key
  if (err.code === 11000) {
    const field = Object.keys(err.keyValue)[0];
    error = new ConflictError(`${field} already exists`);
  }

  // Mongoose validation error
  if (err.name === 'ValidationError') {
    const errors = Object.values(err.errors).map(e => ({
      field: e.path,
      message: e.message,
    }));
    error = new ValidationError('Validation failed', errors);
  }

  // JWT errors
  if (err.name === 'JsonWebTokenError') {
    error = new UnauthorizedError('Invalid token');
  }

  if (err.name === 'TokenExpiredError') {
    error = new UnauthorizedError('Token expired');
  }

  // Send response
  res.status(error.statusCode || 500).json({
    success: false,
    error: {
      message: error.message,
      ...(error.errors && { errors: error.errors }),
    },
  });
};

Request Validation

// Using Joi for validation
const Joi = require('joi');

const validate = (schema) => (req, res, next) => {
  const { error, value } = schema.validate(req.body, {
    abortEarly: false, // Return all errors
    stripUnknown: true, // Remove unknown fields
  });

  if (error) {
    const errors = error.details.map(detail => ({
      field: detail.path.join('.'),
      message: detail.message,
    }));
    throw new ValidationError('Validation failed', errors);
  }

  req.body = value;
  next();
};

// Validation schemas
const schemas = {
  createUser: Joi.object({
    name: Joi.string().min(2).max(50).required(),
    email: Joi.string().email().required(),
    password: Joi.string().min(8).required(),
    age: Joi.number().integer().min(0).max(150),
  }),

  updateUser: Joi.object({
    name: Joi.string().min(2).max(50),
    email: Joi.string().email(),
    age: Joi.number().integer().min(0).max(150),
  }).min(1), // At least one field required
};

// Usage
app.post('/users', validate(schemas.createUser), asyncHandler(async (req, res) => {
  const user = await User.create(req.body);
  res.status(201).json(user);
}));

React Error Handling

Error Boundaries

// components/ErrorBoundary.jsx
import { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Update state to show fallback UI
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // Log error to service
    console.error('Error caught by boundary:', error);
    console.error('Component stack:', errorInfo.componentStack);

    // Send to error tracking service
    // errorTrackingService.log(error, errorInfo);
  }

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

    return this.props.children;
  }
}

// Usage
function App() {
  return (
    <ErrorBoundary fallback={<ErrorPage />}>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={
            <ErrorBoundary fallback={<DashboardError />}>
              <Dashboard />
            </ErrorBoundary>
          } />
        </Routes>
      </Router>
    </ErrorBoundary>
  );
}

React Error Boundary Hook (with react-error-boundary)

// npm install react-error-boundary
import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';

// Fallback component
function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert" className="error-fallback">
      <h2>Something went wrong:</h2>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

// Usage with reset capability
function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error, info) => {
        // Log to error service
        logErrorToService(error, info);
      }}
      onReset={() => {
        // Reset app state
        queryClient.clear();
      }}
    >
      <MainApp />
    </ErrorBoundary>
  );
}

// Using useErrorBoundary hook
function DataComponent() {
  const { showBoundary } = useErrorBoundary();

  const handleClick = async () => {
    try {
      await riskyOperation();
    } catch (error) {
      // Trigger error boundary
      showBoundary(error);
    }
  };

  return <button onClick={handleClick}>Load Data</button>;
}

API Error Handling in React

// hooks/useApi.js
import { useState, useCallback } from 'react';

function useApi() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const request = useCallback(async (apiCall) => {
    setLoading(true);
    setError(null);

    try {
      const result = await apiCall();
      return result;
    } catch (err) {
      const error = {
        message: err.response?.data?.error?.message || err.message,
        status: err.response?.status,
        errors: err.response?.data?.error?.errors,
      };
      setError(error);
      throw error;
    } finally {
      setLoading(false);
    }
  }, []);

  const clearError = useCallback(() => setError(null), []);

  return { loading, error, request, clearError };
}

// Usage
function UserProfile({ userId }) {
  const { loading, error, request, clearError } = useApi();
  const [user, setUser] = useState(null);

  const loadUser = async () => {
    try {
      const data = await request(() => api.get(`/users/${userId}`));
      setUser(data);
    } catch (err) {
      // Error already set by hook
    }
  };

  if (loading) return <Spinner />;

  if (error) {
    return (
      <div className="error">
        <p>{error.message}</p>
        <button onClick={clearError}>Dismiss</button>
        <button onClick={loadUser}>Retry</button>
      </div>
    );
  }

  return <div>{user?.name}</div>;
}

Form Error Handling

// With React Hook Form
import { useForm } from 'react-hook-form';

function SignupForm() {
  const {
    register,
    handleSubmit,
    setError,
    formState: { errors, isSubmitting }
  } = useForm();

  const onSubmit = async (data) => {
    try {
      await api.post('/auth/signup', data);
    } catch (err) {
      // Handle validation errors from server
      if (err.response?.data?.error?.errors) {
        err.response.data.error.errors.forEach(({ field, message }) => {
          setError(field, { type: 'server', message });
        });
      } else {
        // Generic error
        setError('root', {
          type: 'server',
          message: err.response?.data?.error?.message || 'Signup failed'
        });
      }
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {errors.root && (
        <div className="error-banner">{errors.root.message}</div>
      )}

      <div>
        <input
          {...register('email', {
            required: 'Email is required',
            pattern: {
              value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
              message: 'Invalid email format'
            }
          })}
          placeholder="Email"
        />
        {errors.email && (
          <span className="error">{errors.email.message}</span>
        )}
      </div>

      <div>
        <input
          type="password"
          {...register('password', {
            required: 'Password is required',
            minLength: {
              value: 8,
              message: 'Password must be at least 8 characters'
            }
          })}
          placeholder="Password"
        />
        {errors.password && (
          <span className="error">{errors.password.message}</span>
        )}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Signing up...' : 'Sign Up'}
      </button>
    </form>
  );
}

Global Error Handling

Node.js Process Error Handlers

// Handle uncaught exceptions (sync errors)
process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);

  // Log to external service
  // logger.fatal(error);

  // Graceful shutdown
  process.exit(1);
});

// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);

  // Throw to trigger uncaughtException handler
  throw reason;
});

// Handle SIGTERM (graceful shutdown)
process.on('SIGTERM', () => {
  console.log('SIGTERM received. Shutting down gracefully...');

  server.close(() => {
    console.log('Server closed');
    // Close database connections
    mongoose.connection.close(false, () => {
      console.log('MongoDB connection closed');
      process.exit(0);
    });
  });

  // Force close after 30 seconds
  setTimeout(() => {
    console.error('Forcing shutdown...');
    process.exit(1);
  }, 30000);
});

Axios Error Interceptor

// api/axios.js
import axios from 'axios';

const api = axios.create({
  baseURL: process.env.REACT_APP_API_URL,
  timeout: 10000,
});

// Request interceptor
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// Response interceptor
api.interceptors.response.use(
  (response) => response.data,
  (error) => {
    // Network error
    if (!error.response) {
      return Promise.reject({
        message: 'Network error. Please check your connection.',
        isNetworkError: true,
      });
    }

    const { status, data } = error.response;

    // Handle specific status codes
    switch (status) {
      case 401:
        // Token expired or invalid
        localStorage.removeItem('token');
        window.location.href = '/login';
        break;

      case 403:
        // Forbidden - redirect or show message
        break;

      case 404:
        // Not found
        break;

      case 429:
        // Rate limited
        const retryAfter = error.response.headers['retry-after'];
        error.retryAfter = retryAfter;
        break;

      case 500:
        // Server error
        console.error('Server error:', data);
        break;
    }

    return Promise.reject(error);
  }
);

export default api;

Error Logging and Monitoring

// Simple logger
const logger = {
  error: (error, context = {}) => {
    const logEntry = {
      level: 'error',
      message: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString(),
      ...context,
    };

    console.error(JSON.stringify(logEntry));

    // In production, send to logging service
    if (process.env.NODE_ENV === 'production') {
      // sendToLoggingService(logEntry);
    }
  },

  warn: (message, context = {}) => {
    console.warn(JSON.stringify({
      level: 'warn',
      message,
      timestamp: new Date().toISOString(),
      ...context,
    }));
  },

  info: (message, context = {}) => {
    console.log(JSON.stringify({
      level: 'info',
      message,
      timestamp: new Date().toISOString(),
      ...context,
    }));
  },
};

// Usage in error handler
const errorHandler = (err, req, res, next) => {
  logger.error(err, {
    path: req.path,
    method: req.method,
    userId: req.user?.id,
    body: req.body,
    query: req.query,
  });

  // ... send response
};

// React error logging
function logErrorToService(error, errorInfo) {
  const errorLog = {
    message: error.message,
    stack: error.stack,
    componentStack: errorInfo?.componentStack,
    url: window.location.href,
    userAgent: navigator.userAgent,
    timestamp: new Date().toISOString(),
  };

  // Send to backend or error tracking service
  fetch('/api/errors', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(errorLog),
  }).catch(console.error);
}

Best Practices

// 1. Always handle errors - never swallow them
// Bad
try {
  await riskyOperation();
} catch (error) {
  // Silent failure - bad!
}

// Good
try {
  await riskyOperation();
} catch (error) {
  logger.error(error);
  throw error; // Or handle appropriately
}

// 2. Use specific error types
// Bad
throw new Error('Something went wrong');

// Good
throw new NotFoundError('User');
throw new ValidationError('Invalid email', [{ field: 'email', message: '...' }]);

// 3. Include context in errors
// Bad
throw new Error('Database error');

// Good
throw new Error(`Failed to fetch user ${userId}: ${dbError.message}`);

// 4. Fail fast - validate early
function createUser(data) {
  // Validate immediately
  if (!data.email) {
    throw new ValidationError('Email is required');
  }
  if (!isValidEmail(data.email)) {
    throw new ValidationError('Invalid email format');
  }

  // Proceed with operation
  return User.create(data);
}

// 5. Use error boundaries strategically in React
// Wrap different sections separately
<App>
  <ErrorBoundary><Header /></ErrorBoundary>
  <ErrorBoundary><MainContent /></ErrorBoundary>
  <ErrorBoundary><Sidebar /></ErrorBoundary>
</App>

// 6. Provide user-friendly error messages
// Internal: "ECONNREFUSED 127.0.0.1:5432"
// User sees: "Unable to connect to the database. Please try again later."

// 7. Never expose sensitive info in errors
// Bad (production)
res.status(500).json({
  error: error.stack, // Exposes internals!
  query: req.query,
});

// Good
res.status(500).json({
  error: 'Internal server error',
});

Key Takeaways

  • Create custom error classes for different error types
  • Use centralized error handling middleware in Express
  • Wrap async route handlers to catch promise rejections
  • Use Error Boundaries in React for UI error recovery
  • Handle unhandled rejections and uncaught exceptions at process level
  • Log errors with context for debugging
  • Provide user-friendly messages without exposing internals
  • Validate input early to fail fast