JSJavaScript · Lesson 13 of 14

Error Handling & Debugging

JavaScript has one Error type but many error names, a try/catch that silently swallows everything, and async errors that require special handling. The good news: once you understand the three failure modes, it gets simple.

JavaScript
// Error types
try {
  null.property           // TypeError
} catch (e) {
  console.log(e instanceof TypeError)  // true
  console.log(e.name)     // 'TypeError'
  console.log(e.message)  // Cannot read properties of null
  console.log(e.stack)    // full stack trace
}

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

class NetworkError extends Error {
  constructor(message, statusCode) {
    super(message)
    this.name = "NetworkError"
    this.statusCode = statusCode
  }
}

function validateAge(age) {
  if (typeof age !== "number") throw new ValidationError("age", "must be a number")
  if (age < 0 || age > 150)  throw new ValidationError("age", "must be 0-150")
  return true
}

try {
  validateAge(-5)
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(`Field "${e.field}" failed: ${e.message}`)
  } else {
    throw e  // re-throw unexpected errors
  }
}
JavaScript
// Async error handling
// Three patterns: callbacks (old), Promises, async/await

// 1. Promise catch
fetch("https://api.example.com/data")
  .then(res => {
    if (!res.ok) throw new NetworkError("Request failed", res.status)
    return res.json()
  })
  .then(data => console.log(data))
  .catch(err => {
    if (err instanceof NetworkError) {
      console.error(`HTTP ${err.statusCode}: ${err.message}`)
    } else {
      console.error("Unexpected error:", err)
    }
  })
  .finally(() => console.log("Request complete"))

// 2. async/await try/catch (preferred)
async function fetchUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`)
    if (!res.ok) throw new NetworkError("Not found", res.status)
    return await res.json()
  } catch (err) {
    if (err instanceof NetworkError && err.statusCode === 404) {
      return null  // user not found — not an error per se
    }
    throw err     // re-throw everything else
  }
}

// Handling multiple async operations
async function loadDashboard(userId) {
  const results = await Promise.allSettled([
    fetchUser(userId),
    fetch("/api/notifications").then(r => r.json()),
    fetch("/api/stats").then(r => r.json()),
  ])

  for (const [i, result] of results.entries()) {
    if (result.status === "rejected") {
      console.error(`Request ${i} failed:`, result.reason)
    }
  }

  return results
    .filter(r => r.status === "fulfilled")
    .map(r => r.value)
}
⚠ Warning
Never use empty catch blocks: catch (e) {}. This silently swallows errors and makes debugging a nightmare. At minimum, log the error. If you truly want to ignore an error, write a comment explaining why: // ignore AbortError from cancelled fetch.