DEV Community

Alex Chen
Alex Chen

Posted on

Error Handling in JavaScript: The Complete Guide

Error Handling in JavaScript: The Complete Guide

Stop your app from crashing. Handle errors like a pro.

The Basics

// try/catch/finally
try {
  const data = JSON.parse(userInput);
  console.log(data.name);
} catch (error) {
  console.error('Failed to parse:', error.message);
} finally {
  // Always runs — cleanup code here
  console.log('Parse attempt finished');
}

// Throwing errors
throw new Error('Something went wrong');
throw new TypeError('Expected a string');
throw new RangeError('Index out of bounds');
throw new ValidationError('Invalid email'); // Custom!
Enter fullscreen mode Exit fullscreen mode

Custom Error Classes

class AppError extends Error {
  constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    this.code = code;
    this.timestamp = new Date().toISOString();
    Error.captureStackTrace(this, this.constructor);
  }
}

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

class ValidationError extends AppError {
  constructor(fields) {
    super('Validation failed', 422, 'VALIDATION_ERROR');
    this.fields = fields; // { field: ['error1', 'error2'] }
  }
}

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

// Usage
try {
  if (!user) throw new NotFoundError('User');
  if (!isValidEmail(email)) throw new ValidationError({ email: ['Invalid format'] });
} catch (error) {
  if (error instanceof NotFoundError) {
    res.status(404).json({ error: error.message });
  } else if (error instanceof ValidationError) {
    res.status(422).json({ error: error.message, details: error.fields });
  } else {
    res.status(500).json({ error: 'Internal server error' });
  }
}
Enter fullscreen mode Exit fullscreen mode

Async Error Handling

Pattern 1: try/catch with async/await

async function fetchUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);

    if (!response.ok) {
      if (response.status === 404) throw new NotFoundError('User');
      if (response.status === 429) throw new RateLimitError(60);
      throw new AppError(`HTTP ${response.status}`, response.status);
    }

    return await response.json();
  } catch (error) {
    if (error instanceof AppError) throw error;
    throw new AppError(`Network error: ${error.message}`, 503, 'NETWORK_ERROR');
  }
}
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Higher-Order Function Wrapper

// Wrap any async function to always return [data, error]
function asyncHandler(fn) {
  return (...args) => fn(...args).catch(error => [undefined, error]);
}

// Usage — no try/catch needed!
const [user, error] = await asyncHandler(fetchUser)(123);
if (error) return handleError(error);
console.log(user.name);
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Result Type Pattern

class Result {
  static ok(value) { return new Result(null, value); }
  static err(error) { return new Result(error, null); }

  #error;
  #value;

  constructor(error, value) {
    this.#error = error;
    this.#value = value;
  }

  get isOk() { return this.#error === null; }
  get isErr() { return this.#error !== null; }
  get value() { return this.#value; }
  get error() { return this.#error; }

  map(fn) {
    return this.isOk ? Result.ok(fn(this.#value)) : this;
  }

  flatMap(fn) {
    return this.isOk ? fn(this.#value) : this;
  }

  orElse(defaultValue) {
    return this.isOk ? this.#value : defaultValue;
  }
}

// Usage
async function getUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) return Result.err(new Error(`HTTP ${res.status}`));
    return Result.ok(await res.json());
  } catch (e) {
    return Result.err(e);
  }
}

const result = await getUser(123)
  .map(u => u.name)           // Only runs if OK
  .map(name => name.toUpperCase())
  .orElse('Guest');            // Default if error
Enter fullscreen mode Exit fullscreen mode

Global Error Handlers

// Browser
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled promise rejection:', event.reason);
  event.preventDefault(); // Prevent default logging

  // Send to error tracking service
  trackError(event.reason);
});

window.addEventListener('error', (event) => {
  console.error('Uncaught error:', event.error);
  trackError(event.error);
});

// Node.js
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
});

process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);
  // Don't just ignore! Clean up and exit gracefully.
  gracefulShutdown();
});
Enter fullscreen mode Exit fullscreen mode

Express.js Error Handler

// Async handler wrapper for Express routes
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

// Routes using the wrapper
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);
}));

// Global error handler (MUST be last middleware)
app.use((err, req, res, _next) => {
  const isDev = process.env.NODE_ENV !== 'production';

  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: err.message,
      code: err.code,
      ...(isDev && { stack: err.stack }),
    });
  }

  // Unknown errors — don't leak details in production
  console.error('Unexpected error:', err);
  res.status(500).json({
    error: isDev ? err.message : 'Internal server error',
    ...(isDev && { stack: err.stack }),
  });
});
Enter fullscreen mode Exit fullscreen mode

Retry Logic

class RetryableError extends Error {
  constructor(message, retryAfter = 1000) {
    super(message);
    this.name = 'RetryableError';
    this.retryAfter = retryAfter;
  }
}

async function retry(fn, options = {}) {
  const {
    maxAttempts = 3,
    baseDelay = 1000,
    maxDelay = 30000,
    backoffFactor = 2,
    shouldRetry = (err) => err instanceof RetryableError || err.code === 'ECONNRESET',
  } = options;

  let lastError;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      if (!shouldRetry(error) || attempt === maxAttempts) throw error;

      const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt - 1), maxDelay);
      console.warn(`Attempt ${attempt}/${maxAttempts} failed, retrying in ${delay}ms...`);
      await sleep(delay);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Circuit Breaker Pattern

class CircuitBreaker {
  constructor(fn, options = {}) {
    this.fn = fn;
    this.threshold = options.threshold ?? 5;     // Failures before opening
    this.resetTimeout = options.resetTimeout ?? 60000; // ms before trying again
    this.state = 'CLOSED';                      // CLOSED → OPEN → HALF_OPEN
    this.failures = 0;
    this.lastFailureTime = null;
  }

  async execute(...args) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime >= this.resetTimeout) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await this.fn(...args);
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }

  onFailure() {
    this.failures++;
    this.lastFailureTime = Date.now();
    if (this.failures >= this.threshold) {
      this.state = 'OPEN';
    }
  }
}

// Usage
const api = new CircuitBreaker(() => fetch('/api/data'));
await api.execute(); // Normal operation
// After 5 failures → throws "Circuit breaker is OPEN"
// After 60s → allows one request (HALF_OPEN)
Enter fullscreen mode Exit fullscreen mode

How do you handle errors in your apps? Any patterns I missed?

Follow @armorbreak for more JavaScript content.

Top comments (0)