Back to JavaScript tutorials
Basic13 min read

Error Handling & Debugging

Build resilient applications with structured error handling, custom errors, and effective debugging workflows.

Try/Catch/Finally

Wrap code that may throw in `try` blocks. `catch` receives the error object — use `instanceof Error` checks and optional custom error classes to branch handling logic. `finally` runs regardless of success or failure, ideal for releasing resources.

Do not use try/catch for control flow in hot paths. Catch at boundaries — API handlers, event handlers, and async function tops — and translate errors into user-visible messages or logged diagnostics.

  • Catch at system boundaries
  • Use `finally` for cleanup
  • Avoid try/catch for normal branching
try {
  await processPayment(order);
} catch (error) {
  if (error instanceof PaymentError) {
    showToast(error.userMessage);
  }
  throw error;
}

Custom Error Classes

Extend `Error` to create domain-specific errors with extra fields — HTTP status codes, error codes for i18n, or retry hints. Set `this.name` and maintain proper prototype chain with `Object.setPrototypeOf` for instanceof checks across transpilation targets.

Centralize error mapping in middleware or error boundaries so UI layers receive consistent shapes rather than parsing message strings.

  • Extend Error with domain metadata
  • Fix prototype for instanceof
  • Map errors to user messages centrally
class NotFoundError extends Error {
  constructor(resource) {
    super(`${resource} not found`);
    this.name = 'NotFoundError';
  }
}

Console and Logging

Beyond `console.log`, use `console.error` for failures, `console.table` for tabular data, and `console.group` for nested log hierarchies. Structured logging in production should use a logger (pino, winston) with JSON output, log levels, and correlation IDs.

Never log secrets, tokens, or full PII. Scrub payloads at the logging layer and sample verbose debug logs in high-traffic services to control cost.

  • Structured JSON logs in production
  • Correlation IDs across requests
  • Never log secrets or raw PII

Browser and Node Debugging

Use breakpoints, conditional breakpoints, and watch expressions in DevTools. The `debugger` statement pauses execution when DevTools is open — remove before shipping. Source maps connect minified production stacks to original TypeScript.

In Node.js, use `--inspect` for Chrome DevTools attachment. Async stack traces help follow Promise chains. Reproduce production errors locally with matching environment variables and payload fixtures.

  • Source maps for production stacks
  • Conditional breakpoints for rare paths
  • Inspect async stacks for Promise bugs

Defensive Coding Strategies

Validate inputs at module boundaries and fail with clear errors. Use Result/Either patterns or typed error returns in critical paths instead of throwing through deep call stacks when recoverable failure is expected.

Monitor unhandled rejections and uncaught exceptions in production with Sentry, Datadog, or similar. Alert on error rate spikes, not every individual occurrence, to maintain signal-to-noise ratio.

  • Validate early, fail clearly
  • Monitor unhandled rejections
  • Alert on rate spikes, not every error

Get In Touch


Ready to discuss your next project? Drop me a message.