webroad.online
  1. 1Web
  2. 2HTML
  3. 3CSS
  4. 4JavaScript
  5. 5TypeScript
  6. 6Git
  7. 7Tooling
  8. 8React
  9. 9State management
  10. 10Next.js
  11. 11Forms
  12. 12Data and backend
  13. 13SEO
  14. 14Tailwind CSS
  15. 15Animations
  16. 16Testing
  17. 17Architecture
JavaScript · Lesson 13 of 17

Error handling

try/catch/finally, throwing properly, custom errors and when not to catch anything.

Updated

What an error is

An error (exception) is a signal that something went wrong and the program can't continue normally at that point. When it happens, JS stops the current function and "climbs" up the calls until it finds someone who catches it. If nobody does → an error in the console, and in React → the error screen.

An error is an object with:

  • name — the type (TypeError, HttpError);
  • message — what happened;
  • stack — where it happened (the chain of calls) — the most useful piece of information when debugging;
  • cause (optional) — the original error that caused it.

Native error types

Type When it happens Example
TypeError an operation on the wrong type undefined.name, 5()
ReferenceError a variable that doesn't exist / is in the TDZ using an undeclared x
SyntaxError invalid code or invalid JSON JSON.parse('{nope')
RangeError a value out of bounds new Array(-1)

Expected vs unexpected errors

The most important distinction:

Expected Unexpected
Examples an invalid email, an out-of-stock product, a 404 a bug, a server down, undefined.x
How you handle them returned values: { ok: false, error } exceptions: throw
Who sees them the user, with a clear message the logs / monitoring; the user sees a generic message

Validating a form isn't "exceptional" — it's normal for the user to make mistakes. Return a result, don't throw.

try / catch / finally

try {
  const data = JSON.parse(raw)       // code that may throw
  render(data)
} catch (error) {
  showToast('Invalid data')          // runs only if an error happened
} finally {
  setLoading(false)                  // always runs, with or without an error
}

throw — how to throw properly

throw new Error('User not found')                          // ✓ has a stack trace
throw new Error('Request failed', { cause: originalError }) // ✓ keeps the cause
throw 'something'                                          // ✗ no stack, no type

Custom errors

When the code that catches has to tell error types apart:

class HttpError extends Error {
  constructor(status, message) {
    super(message)
    this.name = 'HttpError'
    this.status = status
  }
}

try {
  await api('/users/5')
} catch (e) {
  if (e instanceof HttpError && e.status === 404) return notFound()
  throw e     // what you can't handle, you throw further up
}

Errors in async code

async function load() {
  try {
    const res = await fetch('/api')
    if (!res.ok) throw new HttpError(res.status, 'Request failed')
    return await res.json()
  } catch (e) {
    console.error(e)
    return null
  }
}

A rejected Promise that nobody catches = unhandledrejection. Details in Async.

Where to catch errors — the rule

  1. Catch where you can do something useful: a message for the user, a retry, a fallback value.
  2. Don't catch "so it doesn't crash" — a swallowed error hides the bug.
  3. Never an empty catch {}.
  4. In React / Next: error.tsx is the safety net for whatever slips through.

Summary

  • An error = an object with name, message, stack; it climbs until it's caught.
  • Expected errors → returned values; unexpected ones → throw new Error(...).
  • try/catch/finally; custom errors with extends Error; don't swallow errors.

Official sources

Exercises

Was this page helpful?

One tap — no account needed.