DNA⚡ JavaScriptError Handling & Debugging Patterns
ðŸĢHatchlingJavaScriptError HandlingDebugging

Error Handling & Debugging Patterns

Robust error handling separates production-grade code from demo code. Understanding error propagation, custom errors, async error boundaries, and debugging strategies is essential for senior engineers.

Error Handling & Debugging Patterns

Every application has errors. Senior engineers design systems that handle them gracefully — surfacing actionable information, preventing cascading failures, and maintaining user experience.

The Error Object Model

const err = new Error('Something went wrong');
err.message;     // "Something went wrong"
err.name;        // "Error"
err.stack;       // Stack trace string
err.cause;       // undefined (ES2022 — set via options)

Built-in Error Types

TypeWhen Thrown
TypeErrorWrong type operation (null.prop, calling non-function)
ReferenceErrorAccessing undeclared variable
SyntaxErrorInvalid syntax (usually at parse time)
RangeErrorValue outside valid range (new Array(-1))
URIErrorMalformed URI functions
AggregateErrorMultiple errors (Promise.any rejection)

Custom Error Classes

class AppError extends Error {
  constructor(message, { code, statusCode, cause } = {}) {
    super(message, { cause });
    this.name = 'AppError';
    this.code = code;
    this.statusCode = statusCode;
  }
}
 
class NotFoundError extends AppError {
  constructor(resource, id, options) {
    super(`${resource} with id ${id} not found`, {
      code: 'NOT_FOUND',
      statusCode: 404,
      ...options,
    });
    this.name = 'NotFoundError';
  }
}
 
class ValidationError extends AppError {
  constructor(errors, options) {
    super('Validation failed', {
      code: 'VALIDATION_ERROR',
      statusCode: 400,
      ...options,
    });
    this.name = 'ValidationError';
    this.errors = errors;
  }
}

Error cause (ES2022)

Chain errors to preserve the original failure context:

async function fetchUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (error) {
    throw new AppError('Failed to fetch user', {
      code: 'FETCH_ERROR',
      cause: error,
    });
  }
}

Async Error Handling

Promise Rejection Patterns

async function loadData() {
  const [users, posts] = await Promise.all([
    fetchUsers().catch(err => {
      console.error('Users failed:', err);
      return [];
    }),
    fetchPosts().catch(err => {
      console.error('Posts failed:', err);
      return [];
    }),
  ]);
  return { users, posts };
}

Promise.allSettled for Resilient Batch Operations

const results = await Promise.allSettled([
  fetchUser(1),
  fetchUser(2),
  fetchUser(999),
]);
 
const succeeded = results
  .filter(r => r.status === 'fulfilled')
  .map(r => r.value);
 
const failed = results
  .filter(r => r.status === 'rejected')
  .map(r => r.reason);

Unhandled Rejection Detection

window.addEventListener('unhandledrejection', event => {
  reportError({
    type: 'unhandled_promise_rejection',
    reason: event.reason,
    promise: event.promise,
  });
  event.preventDefault();
});
 
process.on('unhandledRejection', (reason, promise) => {
  logger.error('Unhandled rejection', { reason, promise });
});

Error Boundary Patterns

React Error Boundaries

class ErrorBoundary extends React.Component<
  { fallback: ReactNode; children: ReactNode },
  { hasError: boolean; error: Error | null }
> {
  state = { hasError: false, error: null };
 
  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }
 
  componentDidCatch(error: Error, info: React.ErrorInfo) {
    reportError({ error, componentStack: info.componentStack });
  }
 
  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

Granular Error Boundaries

<ErrorBoundary fallback={<AppCrashScreen />}>
  <Header />
  <ErrorBoundary fallback={<SidebarFallback />}>
    <Sidebar />
  </ErrorBoundary>
  <ErrorBoundary fallback={<ContentError onRetry={refetch} />}>
    <MainContent />
  </ErrorBoundary>
</ErrorBoundary>

The Result Pattern (No-Throw)

Inspired by Rust's Result<T, E>, avoid throwing in expected failure paths:

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };
 
async function safeJson<T>(response: Response): Promise<Result<T>> {
  try {
    if (!response.ok) {
      return { ok: false, error: new Error(`HTTP ${response.status}`) };
    }
    const data = await response.json();
    return { ok: true, value: data as T };
  } catch (error) {
    return { ok: false, error: error as Error };
  }
}
 
const result = await safeJson<User>(response);
if (result.ok) {
  renderUser(result.value);
} else {
  showError(result.error.message);
}

Global Error Monitoring

window.addEventListener('error', event => {
  reportError({
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    colno: event.colno,
    error: event.error,
  });
});
 
window.addEventListener('unhandledrejection', event => {
  reportError({
    type: 'promise_rejection',
    reason: event.reason,
  });
});

Debugging Strategies

Beyond console.log

console.table(arrayOfObjects);
console.group('API Call');
console.time('fetch');
await fetch(url);
console.timeEnd('fetch');
console.groupEnd();
 
console.assert(user.age > 0, 'Age must be positive', user);
console.trace('Execution path');

Conditional Breakpoints

In DevTools, right-click a line → "Add conditional breakpoint" → enter a condition:

user.id === 42 && user.status === 'active'

Performance Profiling

performance.mark('render-start');
renderComponent();
performance.mark('render-end');
performance.measure('render-time', 'render-start', 'render-end');
 
const entries = performance.getEntriesByName('render-time');
console.log(`Render took ${entries[0].duration}ms`);

Interview Signal

Senior candidates demonstrate:

  1. Error hierarchy — Custom error classes with cause chaining, not just throw new Error()
  2. Async awareness — Promise.allSettled for resilience, global rejection handlers
  3. Boundary architecture — Granular error boundaries in React, not one giant wrapper
  4. Result pattern — No-throw alternatives for expected failures vs exceptions for unexpected ones
  5. Monitoring story — Global handlers, structured error reporting, source maps in production