Lesson 13 of 18 · 0 of 2 exercises solved

Error Handling & Debugging

You will learn how to catch and throw , including errors from code. Handling errors well keeps a program from failing silently or crashing.

// 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 sandboxCtrl+Enter runs
// 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)
}
JavaScript sandboxCtrl+Enter runs

Common question

Does catching every error make a program robust?

Errors in JavaScript are objects with a name, a message and a . throw raises one; try/catch/finally handles it; async errors travel as rejected and have to be awaited or caught with .catch. Your own subclasses of Error let handlers tell "the user typed a bad date" apart from "the is down".

Handle errors at the level that can do something about them, log or rethrow the rest, and never leave a catch block empty. Read stack traces from the top frame down, use the debugger and breakpoints for tangled state, and once you find the failing case, write it as a test.

Source: An error that is caught and ignored has not been handled, only hidden. The Node.js documentation swallowing errors among "the most common mistakes".

Common mix-ups

Each of these sounds reasonable. Think about why it is wrong, then open the explanation.

  • Wrapping everything in try/catch prevents crashes, and a program that never crashes is robust.

  • try/catch around an async call catches errors from the promise.

  • Throwing is fine, because catch receives whatever was thrown.

Practice

Write parseJsonSafe(text) that never throws: it returns { ok: true, value } when the parses, and { ok: false, error } with the error message when it does not.

Exercise

A result object instead of a throw

2 checks
function parseJsonSafe(text) {
  return { ok: true, value: JSON.parse(text) }
}

console.log(parseJsonSafe('{"a": 1}'))
console.log(parseJsonSafe('{nope'))
 
JavaScript sandboxCtrl+Enter runs
Output
Press Run to try your code, then Check answer to grade it.

Create ValidationError, a subclass of Error that records which field failed, and validateAge(age) that throws it when age is not an integer between 0 and 150. The error's name should be "ValidationError".

Exercise

A custom error class

4 checks
class ValidationError extends Error {
  constructor(message, field) {
    super(message)
  }
}

function validateAge(age) {
  return age
}

try {
  validateAge(-4)
} catch (err) {
  console.log(err.name, err.field, err.message)
}
 
JavaScript sandboxCtrl+Enter runs
Output
Press Run to try your code, then Check answer to grade it.

Check yourself

Quick check

Why is an empty catch block, catch (e) {}, considered dangerous?

Finished reading?2 exercises still open. You can come back to them later.

Tip: use the ← and → keys to move between lessons.