DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

JavaScript Error Handling: Custom Error Types, Result Pattern, Global Handlers, and Error.cause

JavaScript error handling is often an afterthought — a try/catch here and there. But a robust error handling strategy makes the difference between applications that crash silently and applications that degrade gracefully. Firefox DevTools shows you exactly what went wrong and where.

Error Types

// The built-in error types
new Error('Generic error')           // Base class
new TypeError('Expected a string')   // Wrong type
new RangeError('Index out of bounds') // Value out of range
new ReferenceError('x is not defined') // Undefined variable
new SyntaxError('Unexpected token')  // Invalid syntax (usually parse errors)
new URIError('Malformed URI')       // Bad URI
new EvalError('eval() error')       // eval()-related

// Custom error types
class DatabaseError extends Error {
  constructor(message, { query, code } = {}) {
    super(message);
    this.name = 'DatabaseError';
    this.query = query;
    this.code = code;
  }
}

class NetworkError extends Error {
  constructor(message, { status, url } = {}) {
    super(message);
    this.name = 'NetworkError';
    this.status = status;
    this.url = url;
  }
}
Enter fullscreen mode Exit fullscreen mode

Try/Catch/Finally

try {
  const result = riskyOperation();
  process(result);
} catch (error) {
  if (error instanceof TypeError) {
    // Handle type errors specifically
    console.error('Type error:', error.message);
  } else if (error instanceof NetworkError) {
    // Handle network errors
    showNetworkErrorUI(error);
  } else {
    // Re-throw unknown errors
    throw error;
  }
} finally {
  cleanup(); // Always runs, even if there's an error
}
Enter fullscreen mode Exit fullscreen mode

Async Error Handling

Errors in async functions behave differently:

// Unhandled promise rejection — appears as a red error in DevTools
async function badExample() {
  throw new Error('Oops'); // Creates unhandled rejection if not awaited with try/catch
}
badExample(); // No try/catch = unhandled rejection

// Correct: always handle async errors
async function main() {
  try {
    await badExample();
  } catch (error) {
    console.error('Caught:', error);
  }
}

// Or with .catch()
badExample().catch(error => console.error('Caught:', error));
Enter fullscreen mode Exit fullscreen mode

The Result Pattern (Tuple Pattern)

Instead of throwing, return [value, null] on success and [null, error] on failure:

async function safeJsonParse(text) {
  try {
    return [JSON.parse(text), null];
  } catch (error) {
    return [null, error];
  }
}

async function safeFetch(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      return [null, new NetworkError(`HTTP ${response.status}`, { status: response.status, url })];
    }
    const data = await response.json();
    return [data, null];
  } catch (error) {
    return [null, error];
  }
}

// Usage: no nested try/catch at call sites
const [data, error] = await safeFetch('/api/users');
if (error) {
  handleError(error);
  return;
}
processUsers(data);
Enter fullscreen mode Exit fullscreen mode

Global Error Handling

Catch errors that escape local try/catch:

// Synchronous errors
window.addEventListener('error', (event) => {
  console.error('Global error:', event.error);
  reportToMonitoring({
    message: event.message,
    filename: event.filename,
    line: event.lineno,
    col: event.colno,
    stack: event.error?.stack,
  });
  return false; // Don't suppress default browser error handling
});

// Unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled rejection:', event.reason);
  reportToMonitoring({
    message: String(event.reason),
    stack: event.reason?.stack,
    type: 'unhandledrejection',
  });
  event.preventDefault(); // Suppress default browser warning (optional)
});
Enter fullscreen mode Exit fullscreen mode

Error Boundaries in React

For React apps, use error boundaries to prevent the whole app from unmounting:

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

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

  componentDidCatch(error, info) {
    reportToMonitoring({ error, componentStack: info.componentStack });
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <div>Something went wrong.</div>;
    }
    return this.props.children;
  }
}

// Usage
<ErrorBoundary fallback={<ErrorPage />}>
  <App />
</ErrorBoundary>
Enter fullscreen mode Exit fullscreen mode

Error Stack Traces

Stack traces tell you where the error originated:

function outer() {
  inner();
}

function inner() {
  throw new Error('Error in inner');
}

outer();
// Error: Error in inner
//     at inner (app.js:7)
//     at outer (app.js:3)
//     at <anonymous> (app.js:10)
Enter fullscreen mode Exit fullscreen mode

Firefox DevTools shows this in the Console. Click any filename in the stack trace to jump to that line in the Debugger.

Preserve stack traces across async boundaries:

// Wrapping an error: attach the cause
async function fetchUser(id) {
  try {
    return await fetch(`/api/user/${id}`).then(r => r.json());
  } catch (cause) {
    throw new Error(`Failed to fetch user ${id}`, { cause }); // ES2022
  }
}

// The cause property lets you trace the original error
try {
  await fetchUser(123);
} catch (error) {
  console.error(error.message);  // 'Failed to fetch user 123'
  console.error(error.cause);    // Original fetch error
}
Enter fullscreen mode Exit fullscreen mode

Retry with Error Classification

Not all errors are worth retrying:

function isRetryable(error) {
  if (error instanceof NetworkError) {
    return error.status >= 500 || error.status === 429; // Server errors, rate limit
  }
  if (error.name === 'AbortError') return false; // User cancelled
  if (error instanceof TypeError) return false;  // Programming error
  return true; // Default: retry
}

async function withRetry(fn, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxAttempts || !isRetryable(error)) throw error;
      await new Promise(r => setTimeout(r, 1000 * attempt)); // Backoff
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Firefox DevTools: Error Debugging

Firefox DevTools Console:

  1. Errors appear in red with the message and file:line
  2. Click the file link to jump to the Debugger at that exact line
  3. Pause on exceptions: Debugger → click the pause-on-exception button (stop sign icon)
  4. Firefox pauses execution exactly when the error is thrown — inspect local variables before the stack unwinds

The "Pause on caught exceptions" option pauses even inside try/catch blocks.

Top comments (0)